feat(admin, admin-ui, medusa-js, medusa-react, medusa): Support Admin Extensions (#4761)

Co-authored-by: Rares Stefan <948623+StephixOne@users.noreply.github.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Kasper Fabricius Kristensen
2023-08-17 14:14:45 +02:00
committed by GitHub
parent 26c78bbc03
commit f1a05f4725
189 changed files with 14570 additions and 12773 deletions
@@ -6,25 +6,30 @@ import {
import { useEffect, useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spacer from "../../../components/atoms/spacer"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
import Actionables from "../../../components/molecules/actionables"
import JSONView from "../../../components/molecules/json-view"
import DeletePrompt from "../../../components/organisms/delete-prompt"
import { MetadataField } from "../../../components/organisms/metadata"
import RawJSON from "../../../components/organisms/raw-json"
import Section from "../../../components/organisms/section"
import CollectionModal from "../../../components/templates/collection-modal"
import AddProductsTable from "../../../components/templates/collection-product-table/add-product-table"
import ViewProductsTable from "../../../components/templates/collection-product-table/view-products-table"
import useNotification from "../../../hooks/use-notification"
import { useWidgets } from "../../../providers/widget-provider"
import Medusa from "../../../services/api"
import { getErrorMessage } from "../../../utils/error-messages"
import { getErrorStatus } from "../../../utils/get-error-status"
const CollectionDetails = () => {
const { id } = useParams()
const { collection, isLoading, refetch } = useAdminCollection(id!)
const { collection, isLoading, error, refetch } = useAdminCollection(id!)
const deleteCollection = useAdminDeleteCollection(id!)
const updateCollection = useAdminUpdateCollection(id!)
const [showEdit, setShowEdit] = useState(false)
@@ -105,6 +110,32 @@ const CollectionDetails = () => {
}
}, [collection?.products])
const { getWidgets } = useWidgets()
if (error) {
const errorStatus = getErrorStatus(error)
if (errorStatus) {
// If the product is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
return null
}
}
// Let the error boundary handle the error
throw error
}
if (isLoading || !collection) {
// temp, perhaps use skeletons?
return (
<div className="flex h-[calc(100vh-64px)] w-full items-center justify-center">
<Spinner variant="secondary" />
</div>
)
}
return (
<>
<div className="flex flex-col">
@@ -113,12 +144,19 @@ const CollectionDetails = () => {
path="/a/products?view=collections"
label="Back to Collections"
/>
<div className="rounded-rounded py-large px-xlarge border-grey-20 bg-grey-0 mb-large border">
{isLoading || !collection ? (
<div className="flex h-12 w-full items-center">
<Spinner variant="secondary" size="large" />
</div>
) : (
<div className="gap-y-xsmall flex flex-col">
{getWidgets("product_collection.details.before").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={collection}
injectionZone="product_collection.details.before"
widget={w}
/>
)
})}
<div className="rounded-rounded py-large px-xlarge border-grey-20 bg-grey-0 border">
<div>
<div>
<div className="flex items-center justify-between">
@@ -155,29 +193,44 @@ const CollectionDetails = () => {
</div>
)}
</div>
)}
</div>
<Section
title="Products"
actions={[
{
label: "Edit Products",
icon: <EditIcon size="20" />,
onClick: () => setShowAddProducts(!showAddProducts),
},
]}
>
<p className="text-grey-50 inter-base-regular mt-xsmall mb-base">
Products in this collection
</p>
{collection && (
<ViewProductsTable
key={updates} // force re-render when collection is updated
collectionId={collection.id}
refetchCollection={refetch}
/>
)}
</Section>
{getWidgets("product_collection.details.after").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={collection}
injectionZone="product_collection.details.after"
widget={w}
/>
)
})}
<RawJSON data={collection} title="Raw collection" />
</div>
<Section
title="Products"
actions={[
{
label: "Edit Products",
icon: <EditIcon size="20" />,
onClick: () => setShowAddProducts(!showAddProducts),
},
]}
>
<p className="text-grey-50 inter-base-regular mt-xsmall mb-base">
To start selling, all you need is a name, price, and image.
</p>
{collection && (
<ViewProductsTable
key={updates} // force re-render when collection is updated
collectionId={collection.id}
refetchCollection={refetch}
/>
)}
</Section>
<Spacer />
</div>
{showEdit && (
<CollectionModal
@@ -1,10 +1,25 @@
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../components/extensions/route-container"
import { useRoutes } from "../../providers/route-provider"
import CollectionDetails from "./details"
const Collections = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/collections")
return (
<Routes>
<Route path="/:id" element={<CollectionDetails />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/collections"} />}
/>
)
})}
</Routes>
)
}
@@ -1,10 +1,11 @@
import { useAdminCustomer } from "medusa-react"
import moment from "moment"
import { useState } from "react"
import { useParams } from "react-router-dom"
import { useNavigate, useParams } from "react-router-dom"
import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import StatusDot from "../../../components/fundamentals/status-indicator"
import Actionables, {
@@ -14,12 +15,15 @@ import BodyCard from "../../../components/organisms/body-card"
import RawJSON from "../../../components/organisms/raw-json"
import Section from "../../../components/organisms/section"
import CustomerOrdersTable from "../../../components/templates/customer-orders-table"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorStatus } from "../../../utils/get-error-status"
import EditCustomerModal from "./edit"
const CustomerDetail = () => {
const { id } = useParams()
const navigate = useNavigate()
const { customer, isLoading } = useAdminCustomer(id!)
const { customer, isLoading, error } = useAdminCustomer(id!)
const [showEdit, setShowEdit] = useState(false)
const customerName = () => {
@@ -38,6 +42,31 @@ const CustomerDetail = () => {
},
]
const { getWidgets } = useWidgets()
if (error) {
const errorStatus = getErrorStatus(error)
if (errorStatus) {
// If the product is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
return null
}
}
// Let the error boundary handle the error
throw error
}
if (isLoading || !customer) {
return (
<div className="flex h-[calc(100vh-64px)] w-full items-center justify-center">
<Spinner variant="secondary" />
</div>
)
}
return (
<div>
<BackButton
@@ -46,6 +75,17 @@ const CustomerDetail = () => {
className="mb-xsmall"
/>
<div className="gap-y-xsmall flex flex-col">
{getWidgets("customer.details.before").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={customer}
injectionZone="customer.details.before"
widget={w}
/>
)
})}
<Section>
<div className="flex w-full items-start justify-between">
<div className="gap-x-base flex w-full items-center">
@@ -61,7 +101,7 @@ const CustomerDetail = () => {
{customerName()}
</h1>
<h3 className="inter-small-regular text-grey-50">
{customer?.email}
{customer.email}
</h3>
</div>
</div>
@@ -72,21 +112,21 @@ const CustomerDetail = () => {
<div className="inter-smaller-regular text-grey-50 mb-1">
First seen
</div>
<div>{moment(customer?.created_at).format("DD MMM YYYY")}</div>
<div>{moment(customer.created_at).format("DD MMM YYYY")}</div>
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Phone
</div>
<div className="max-w-[200px] truncate">
{customer?.phone || "N/A"}
{customer.phone || "N/A"}
</div>
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Orders
</div>
<div>{customer?.orders.length}</div>
<div>{customer.orders.length}</div>
</div>
<div className="h-100 flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
@@ -94,28 +134,33 @@ const CustomerDetail = () => {
</div>
<div className="h-50 flex items-center justify-center">
<StatusDot
variant={customer?.has_account ? "success" : "danger"}
title={customer?.has_account ? "Registered" : "Guest"}
variant={customer.has_account ? "success" : "danger"}
title={customer.has_account ? "Registered" : "Guest"}
/>
</div>
</div>
</div>
</Section>
<BodyCard
title={`Orders (${customer?.orders.length})`}
title={`Orders (${customer.orders.length})`}
subtitle="An overview of Customer Orders"
>
{isLoading || !customer ? (
<div className="pt-2xlarge flex w-full items-center justify-center">
<Spinner size={"large"} variant={"secondary"} />
</div>
) : (
<div className="flex grow flex-col">
<CustomerOrdersTable id={customer.id} />
</div>
)}
<div className="flex grow flex-col">
<CustomerOrdersTable id={customer.id} />
</div>
</BodyCard>
{getWidgets("customer.details.after").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={customer}
injectionZone="customer.details.after"
widget={w}
/>
)
})}
<RawJSON data={customer} title="Raw customer" />
</div>
@@ -11,6 +11,8 @@ import { useEffect, useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
@@ -21,6 +23,8 @@ import CustomersListTable from "../../../components/templates/customer-group-tab
import EditCustomersTable from "../../../components/templates/customer-group-table/edit-customers-table"
import useQueryFilters from "../../../hooks/use-query-filters"
import useToggleState from "../../../hooks/use-toggle-state"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorStatus } from "../../../utils/get-error-status"
import CustomerGroupModal from "./customer-group-modal"
/**
@@ -127,7 +131,7 @@ function CustomerGroupCustomersList(props: CustomerGroupCustomersListProps) {
<BodyCard
title="Customers"
actionables={actions}
className="my-4 min-h-[756px] w-full"
className="min-h-[756px] w-full"
>
{showCustomersModal && (
<EditCustomersTable
@@ -229,11 +233,32 @@ function CustomerGroupDetailsHeader(props: CustomerGroupDetailsHeaderProps) {
*/
function CustomerGroupDetails() {
const { id } = useParams()
const navigate = useNavigate()
const { customer_group } = useAdminCustomerGroup(id!)
const { customer_group, isLoading, error } = useAdminCustomerGroup(id!)
const { getWidgets } = useWidgets()
if (!customer_group) {
return null
if (error) {
const errorStatus = getErrorStatus(error)
if (errorStatus) {
// If the product is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
return null
}
}
// Let the error boundary handle the error
throw error
}
if (isLoading || !customer_group) {
return (
<div className="flex h-[calc(100vh-64px)] w-full items-center justify-center">
<Spinner variant="secondary" />
</div>
)
}
return (
@@ -243,8 +268,32 @@ function CustomerGroupDetails() {
label="Back to customer groups"
className="mb-4"
/>
<CustomerGroupDetailsHeader customerGroup={customer_group} />
<CustomerGroupCustomersList group={customer_group} />
<div className="gap-y-xsmall flex flex-col">
{getWidgets("customer_group.details.before").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={customer_group}
injectionZone="customer_group.details.before"
widget={w}
/>
)
})}
<CustomerGroupDetailsHeader customerGroup={customer_group} />
{getWidgets("customer_group.details.after").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={customer_group}
injectionZone="customer_group.details.after"
widget={w}
/>
)
})}
<CustomerGroupCustomersList group={customer_group} />
</div>
</div>
)
}
@@ -1,8 +1,12 @@
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../../components/extensions/route-container"
import WidgetContainer from "../../../components/extensions/widget-container"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../../components/organisms/body-card"
import CustomerGroupsTable from "../../../components/templates/customer-group-table/customer-groups-table"
import useToggleState from "../../../hooks/use-toggle-state"
import { useRoutes } from "../../../providers/route-provider"
import { useWidgets } from "../../../providers/widget-provider"
import CustomersPageTableHeader from "../header"
import CustomerGroupModal from "./customer-group-modal"
import Details from "./details"
@@ -12,6 +16,7 @@ import Details from "./details"
*/
function Index() {
const { state, open, close } = useToggleState()
const { getWidgets } = useWidgets()
const actions = [
{
@@ -27,7 +32,18 @@ function Index() {
return (
<>
<div className="flex h-full grow flex-col">
<div className="gap-y-xsmall flex h-full grow flex-col">
{getWidgets("customer_group.list.before").map((w, index) => {
return (
<WidgetContainer
key={index}
entity={null}
widget={w}
injectionZone="customer_group.list.before"
/>
)
})}
<BodyCard
actionables={actions}
className="h-auto"
@@ -35,6 +51,17 @@ function Index() {
>
<CustomerGroupsTable />
</BodyCard>
{getWidgets("customer_group.list.after").map((w, index) => {
return (
<WidgetContainer
key={index}
entity={null}
widget={w}
injectionZone="customer_group.list.after"
/>
)
})}
</div>
<CustomerGroupModal open={state} onClose={close} />
</>
@@ -45,10 +72,25 @@ function Index() {
* Customer groups routes
*/
function CustomerGroups() {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/customers/groups")
return (
<Routes>
<Route index element={<Index />} />
<Route path="/:id" element={<Details />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={
<RouteContainer route={r} previousPath={"/customers/groups"} />
}
/>
)
})}
</Routes>
)
}
@@ -1,31 +1,72 @@
import { Route, Routes } from "react-router-dom"
import Spacer from "../../components/atoms/spacer"
import RouteContainer from "../../components/extensions/route-container"
import WidgetContainer from "../../components/extensions/widget-container"
import BodyCard from "../../components/organisms/body-card"
import CustomerTable from "../../components/templates/customer-table"
import { useRoutes } from "../../providers/route-provider"
import { useWidgets } from "../../providers/widget-provider"
import Details from "./details"
import CustomerGroups from "./groups"
import CustomersPageTableHeader from "./header"
const CustomerIndex = () => {
const { getWidgets } = useWidgets()
return (
<div>
<div className="gap-y-xsmall flex flex-col">
{getWidgets("customer.list.before").map((w, index) => {
return (
<WidgetContainer
key={index}
entity={null}
widget={w}
injectionZone="customer.list.before"
/>
)
})}
<BodyCard
customHeader={<CustomersPageTableHeader activeView="customers" />}
className="h-fit"
>
<CustomerTable />
</BodyCard>
{getWidgets("customer.list.after").map((w, index) => {
return (
<WidgetContainer
key={index}
entity={null}
widget={w}
injectionZone="customer.list.after"
/>
)
})}
<Spacer />
</div>
)
}
const Customers = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/customers")
return (
<Routes>
<Route index element={<CustomerIndex />} />
<Route path="/groups/*" element={<CustomerGroups />} />
<Route path="/:id" element={<Details />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/customers"} />}
/>
)
})}
</Routes>
)
}
@@ -1,12 +1,15 @@
import { useAdminDeleteDiscount, useAdminDiscount } from "medusa-react"
import { useState } from "react"
import { useParams } from "react-router-dom"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import DeletePrompt from "../../../components/organisms/delete-prompt"
import RawJSON from "../../../components/organisms/raw-json"
import useNotification from "../../../hooks/use-notification"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorMessage } from "../../../utils/error-messages"
import { getErrorStatus } from "../../../utils/get-error-status"
import { DiscountFormProvider } from "../new/discount-form/form/discount-form-context"
import DiscountDetailsConditions from "./conditions"
import Configurations from "./configurations"
@@ -14,8 +17,9 @@ import General from "./general"
const Edit = () => {
const { id } = useParams()
const navigate = useNavigate()
const { discount, isLoading } = useAdminDiscount(
const { discount, isLoading, error } = useAdminDiscount(
id!,
{ expand: "rule,rule.conditions" },
{
@@ -26,6 +30,8 @@ const Edit = () => {
const deleteDiscount = useAdminDeleteDiscount(id!)
const notification = useNotification()
const { getWidgets } = useWidgets()
const handleDelete = () => {
deleteDiscount.mutate(undefined, {
onSuccess: () => {
@@ -37,6 +43,29 @@ const Edit = () => {
})
}
if (error) {
const errorStatus = getErrorStatus(error)
if (errorStatus) {
// If the discount is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
return null
}
}
// Let the error boundary handle the error
throw error
}
if (isLoading || !discount) {
return (
<div className="flex h-[calc(100vh-64px)] w-full items-center justify-center">
<Spinner variant="secondary" />
</div>
)
}
return (
<div className="pb-xlarge">
{showDelete && (
@@ -55,20 +84,34 @@ const Edit = () => {
path="/a/discounts"
className="mb-xsmall"
/>
{isLoading || !discount ? (
<div className="flex h-full items-center justify-center">
<Spinner variant="secondary" />
</div>
) : (
<div className="gap-y-xsmall flex flex-col">
<DiscountFormProvider>
<General discount={discount} />
<Configurations discount={discount} />
<DiscountDetailsConditions discount={discount} />
<RawJSON data={discount} title="Raw discount" />
</DiscountFormProvider>
</div>
)}
<div className="gap-y-xsmall flex flex-col">
<DiscountFormProvider>
{getWidgets("discount.details.before").map((w, index) => {
return (
<WidgetContainer
key={index}
entity={discount}
widget={w}
injectionZone="discount.details.before"
/>
)
})}
<General discount={discount} />
<Configurations discount={discount} />
<DiscountDetailsConditions discount={discount} />
{getWidgets("discount.details.after").map((w, index) => {
return (
<WidgetContainer
key={index}
entity={discount}
widget={w}
injectionZone="discount.details.after"
/>
)
})}
<RawJSON data={discount} title="Raw discount" />
</DiscountFormProvider>
</div>
</div>
)
}
@@ -2,10 +2,14 @@ import { useState } from "react"
import { Route, Routes } from "react-router-dom"
import Fade from "../../components/atoms/fade-wrapper"
import Spacer from "../../components/atoms/spacer"
import RouteContainer from "../../components/extensions/route-container"
import WidgetContainer from "../../components/extensions/widget-container"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header"
import DiscountTable from "../../components/templates/discount-table"
import { useRoutes } from "../../providers/route-provider"
import { useWidgets } from "../../providers/widget-provider"
import Details from "./details"
import New from "./new"
import DiscountForm from "./new/discount-form"
@@ -22,9 +26,21 @@ const DiscountIndex = () => {
},
]
const { getWidgets } = useWidgets()
return (
<div className="flex h-full flex-col">
<div className="flex w-full grow flex-col">
<div className="gap-y-xsmall flex w-full grow flex-col">
{getWidgets("discount.list.before").map((w, index) => {
return (
<WidgetContainer
key={index}
widget={w}
injectionZone="discount.list.before"
entity={null}
/>
)
})}
<BodyCard
actionables={actionables}
customHeader={<TableViewHeader views={["discounts"]} />}
@@ -32,6 +48,16 @@ const DiscountIndex = () => {
>
<DiscountTable />
</BodyCard>
{getWidgets("discount.list.after").map((w, index) => {
return (
<WidgetContainer
key={index}
widget={w}
injectionZone="discount.list.after"
entity={null}
/>
)
})}
<Spacer />
</div>
<DiscountFormProvider>
@@ -44,11 +70,24 @@ const DiscountIndex = () => {
}
const Discounts = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/discounts")
return (
<Routes>
<Route index element={<DiscountIndex />} />
<Route path="/new" element={<New />} />
<Route path="/:id" element={<Details />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/discounts"} />}
/>
)
})}
</Routes>
)
}
@@ -3,6 +3,7 @@ import moment from "moment"
import { useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-icon"
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
import StatusSelector from "../../../components/molecules/status-selector"
@@ -10,6 +11,7 @@ import BodyCard from "../../../components/organisms/body-card"
import RawJSON from "../../../components/organisms/raw-json"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorMessage } from "../../../utils/error-messages"
import { formatAmountWithSymbol } from "../../../utils/prices"
import EditGiftCardModal from "./edit-gift-card-modal"
@@ -24,6 +26,8 @@ const GiftCardDetails = () => {
const updateGiftCard = useAdminUpdateGiftCard(giftCard?.id!)
const { getWidgets } = useWidgets()
const notification = useNotification()
const {
@@ -81,6 +85,17 @@ const GiftCardDetails = () => {
) : (
<>
<div className="gap-y-xsmall flex flex-col">
{getWidgets("custom_gift_card.before").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
entity={giftCard}
injectionZone="custom_gift_card.before"
/>
)
})}
<BodyCard
className={"h-auto min-h-0 w-full"}
title={`${giftCard?.code}`}
@@ -147,6 +162,18 @@ const GiftCardDetails = () => {
</div>
</div>
</BodyCard>
{getWidgets("custom_gift_card.after").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
entity={giftCard}
injectionZone="custom_gift_card.after"
/>
)
})}
<RawJSON data={giftCard} title="Raw gift card" />
</div>
@@ -1,14 +1,29 @@
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../components/extensions/route-container"
import { useRoutes } from "../../providers/route-provider"
import GiftCardDetails from "./details"
import ManageGiftCard from "./manage"
import Overview from "./overview"
const GiftCard = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/gift-cards")
return (
<Routes>
<Route path="/" element={<Overview />} />
<Route path="/:id" element={<GiftCardDetails />} />
<Route path="/manage" element={<ManageGiftCard />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/gift-cards"} />}
/>
)
})}
</Routes>
)
}
@@ -3,12 +3,14 @@ import { useAdminProducts } from "medusa-react"
import { useNavigate } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import GiftCardDenominationsSection from "../../../components/organisms/gift-card-denominations-section"
import ProductAttributesSection from "../../../components/organisms/product-attributes-section"
import ProductGeneralSection from "../../../components/organisms/product-general-section"
import ProductMediaSection from "../../../components/organisms/product-media-section"
import ProductRawSection from "../../../components/organisms/product-raw-section"
import ProductThumbnailSection from "../../../components/organisms/product-thumbnail-section"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorStatus } from "../../../utils/get-error-status"
const Manage = () => {
@@ -25,6 +27,8 @@ const Manage = () => {
const giftCard = products?.[0] as Product | undefined
const { getWidgets } = useWidgets()
if (!giftCard) {
return (
<div className="flex h-screen w-full items-center justify-center">
@@ -57,9 +61,34 @@ const Manage = () => {
/>
<div className="gap-x-base grid grid-cols-12">
<div className="gap-y-xsmall col-span-8 flex flex-col">
{getWidgets("gift_card.details.before").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone={"gift_card.details.before"}
entity={giftCard}
/>
)
})}
<ProductGeneralSection product={giftCard} />
<GiftCardDenominationsSection giftCard={giftCard} />
<ProductAttributesSection product={giftCard} />
{getWidgets("gift_card.details.after").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone={"gift_card.details.after"}
entity={giftCard}
/>
)
})}
<ProductRawSection product={giftCard} />
</div>
<div className="gap-y-xsmall col-span-4 flex flex-col">
@@ -10,6 +10,7 @@ import { useNavigate } from "react-router-dom"
import PageDescription from "../../components/atoms/page-description"
import Spacer from "../../components/atoms/spacer"
import Spinner from "../../components/atoms/spinner"
import WidgetContainer from "../../components/extensions/widget-container"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BannerCard from "../../components/molecules/banner-card"
import BodyCard from "../../components/organisms/body-card"
@@ -18,6 +19,7 @@ import GiftCardBanner from "../../components/organisms/gift-card-banner"
import GiftCardTable from "../../components/templates/gift-card-table"
import useNotification from "../../hooks/use-notification"
import useToggleState from "../../hooks/use-toggle-state"
import { useWidgets } from "../../providers/widget-provider"
import { ProductStatus } from "../../types/shared"
import { getErrorMessage } from "../../utils/error-messages"
import CustomGiftcard from "./custom-giftcard"
@@ -94,6 +96,8 @@ const Overview = () => {
}
}, [giftCard, store])
const { getWidgets } = useWidgets()
return (
<>
<div className="flex flex-col">
@@ -103,6 +107,16 @@ const Overview = () => {
/>
{!isLoading ? (
<div className="gap-y-xsmall flex flex-col">
{getWidgets("gift_card.list.before").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone="gift_card.list.before"
entity={null}
/>
)
})}
{giftCardWithCurrency ? (
<GiftCardBanner
{...giftCardWithCurrency}
@@ -130,6 +144,17 @@ const Overview = () => {
>
<GiftCardTable />
</BodyCard>
{getWidgets("gift_card.list.after").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone="gift_card.list.after"
entity={null}
/>
)
})}
</div>
) : (
<div className="rounded-rounded border-grey-20 flex h-44 w-full items-center justify-center border">
@@ -1,59 +0,0 @@
import { Order } from "@medusajs/medusa"
import { renderHook, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm, UseFormReturn } from "react-hook-form"
import ClaimTypeForm from ".."
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { getDefaultClaimValues } from "../../../details/utils/get-default-values"
describe("ClaimTypeForm", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const order = fixtures.get("order") as unknown as Order
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: getDefaultClaimValues(order),
})
)
form = result.current
renderWithProviders(<ClaimTypeForm form={nestedForm(form, "claim_type")} />)
})
it("should render correctly with the initial value of refund", async () => {
const {
claim_type: { type },
} = form.getValues()
expect(screen.getByText("Refund")).toBeInTheDocument()
expect(screen.getByText("Replace")).toBeInTheDocument()
expect(type).toEqual("refund")
})
it("should update the value of the form when a new type is selected", async () => {
const {
claim_type: { type: initialType },
} = form.getValues()
const user = userEvent.setup()
expect(initialType).toEqual("refund")
const replace = screen.getByLabelText("Replace")
await user.click(replace)
const {
claim_type: { type },
} = form.getValues()
expect(type).toEqual("replace")
})
})
@@ -1,59 +0,0 @@
import { Order, Return } from "@medusajs/medusa"
import { renderHook, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm, UseFormReturn } from "react-hook-form"
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { ReceiveReturnFormType } from "../../../details/receive-return"
import { getDefaultReceiveReturnValues } from "../../../details/utils/get-default-values"
import { ItemsToReceiveForm } from "../items-to-receive-form"
describe("ItemsToReceiveForm with ReceiveReturnMenu", () => {
let form: UseFormReturn<ReceiveReturnFormType, any>
beforeEach(() => {
const order = fixtures.get("order") as unknown as Order
const return_ = fixtures.get("return") as unknown as Return
const { result } = renderHook(() =>
useForm<ReceiveReturnFormType>({
defaultValues: getDefaultReceiveReturnValues(order, return_),
})
)
form = result.current
renderWithProviders(
<ItemsToReceiveForm
form={nestedForm(form, "receive_items")}
order={order}
/>
)
})
it("should render correctly", async () => {
expect(screen.getByText("Items to receive")).toBeInTheDocument()
expect(screen.getByText("Medusa Shorts")).toBeInTheDocument()
expect(screen.getByText("S")).toBeInTheDocument()
expect(screen.getByText("1")).toBeInTheDocument()
})
it("should mark an item as to be received when checkbox is checked", async () => {
const checkboxes = screen.getAllByRole("checkbox")
const user = userEvent.setup()
// We expect two checkboxes, one for the header and one for the item
expect(checkboxes).toHaveLength(2)
// Item checkbox
const checkbox = checkboxes[1]
expect(checkbox).not.toBeChecked()
await user.click(checkbox)
const { receive_items } = form.getValues()
expect(checkbox).toBeChecked()
expect(receive_items.items[0].receive).toEqual(true)
})
})
@@ -1,119 +0,0 @@
import { Order } from "@medusajs/medusa"
import { renderHook, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm, UseFormReturn } from "react-hook-form"
import ItemsToReturnForm from ".."
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { getDefaultClaimValues } from "../../../details/utils/get-default-values"
const order = fixtures.get("order") as unknown as Order
describe("ItemsToSendForm with RegisterClaimMenu", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: getDefaultClaimValues(order),
})
)
form = result.current
renderWithProviders(
<ItemsToReturnForm
form={nestedForm(form, "return_items")}
order={order}
/>
)
})
it("should render correctly", async () => {
const titles = order.returnable_items?.map((item) => item.title)
// expect all titles in titles array to appear at least once in the document
titles?.forEach((title) => {
expect(screen.getAllByText(title).length).toBeGreaterThan(0)
})
})
it("should initially not display any items as marked for return", async () => {
const checkboxes = screen.getAllByRole("checkbox")
checkboxes.forEach((checkbox) => {
expect(checkbox).not.toBeChecked()
})
})
it("should mark all item as to be returned when checkbox is checked", async () => {
const checkboxes = screen.getAllByRole("checkbox")
// Checkbox to select all items
const checkbox = checkboxes[0]
const user = userEvent.setup()
await user.click(checkbox)
expect(checkbox).toBeChecked()
const { return_items } = form.getValues()
// expect all items to be marked for return
for (const item of return_items.items) {
expect(item.return).toBeTruthy()
}
})
it("should only mark the first item as to be returned", async () => {
const checkboxes = screen.getAllByRole("checkbox")
// Checkbox to select first item
const checkbox = checkboxes[1]
const user = userEvent.setup()
await user.click(checkbox)
expect(checkbox).toBeChecked()
const { return_items } = form.getValues()
// expect first item to be marked for return
expect(return_items.items[0].return).toBeTruthy()
// expect all other items to not be marked for return
for (const item of return_items.items.slice(1)) {
expect(item.return).toBeFalsy()
}
})
it("should update quantity correctly", async () => {
const checkboxes = screen.getAllByRole("checkbox")
const checkbox = checkboxes[1]
const user = userEvent.setup()
await user.click(checkbox)
expect(checkbox).toBeChecked()
const decrement = screen.getByLabelText("Decrease quantity")
await user.click(decrement)
const { return_items } = form.getValues()
expect(return_items.items[0].quantity).toEqual(1)
const increment = screen.getByLabelText("Increase quantity")
await user.click(increment)
// should return to initial quantity
expect(return_items.items[0].quantity).toEqual(2)
})
})
@@ -1,80 +0,0 @@
import { Order } from "@medusajs/medusa"
import { renderHook, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm, UseFormReturn } from "react-hook-form"
import ItemsToSendForm from ".."
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { getDefaultClaimValues } from "../../../details/utils/get-default-values"
const order = fixtures.get("order") as unknown as Order
describe("ItemsToSendForm with RegisterClaimMenu", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: getDefaultClaimValues(order),
})
)
form = result.current
form.setValue("additional_items.items", [
{
in_stock: 100,
original_price: 10000,
price: 10000,
product_title: "Test",
quantity: 1,
variant_id: "test",
variant_title: "Test",
},
])
renderWithProviders(
<ItemsToSendForm
form={nestedForm(form, "additional_items")}
order={order}
/>
)
})
it("should render correctly", async () => {
expect(screen.getByText("Items to send")).toBeInTheDocument()
expect(screen.getByText("Add products")).toBeInTheDocument()
})
it("should display products to send correctly", async () => {
expect(screen.getByText("Test")).toBeInTheDocument()
expect(screen.getByText("€100.00")).toBeInTheDocument()
expect(screen.getByText("1")).toBeInTheDocument()
})
it("should update quantity correctly", async () => {
const { additional_items } = form.getValues()
const user = userEvent.setup()
const increment = screen.getByLabelText("Increase quantity")
await user.click(increment)
expect(screen.getByText("2")).toBeInTheDocument()
expect(additional_items.items[0].quantity).toEqual(2)
await user.click(increment)
expect(screen.getByText("3")).toBeInTheDocument()
expect(additional_items.items[0].quantity).toEqual(3)
const decrement = screen.getByLabelText("Decrease quantity")
await user.click(decrement)
expect(screen.getByText("2")).toBeInTheDocument()
expect(additional_items.items[0].quantity).toEqual(2)
})
})
@@ -1,59 +0,0 @@
import { Order } from "@medusajs/medusa"
import { fireEvent, renderHook, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm, UseFormReturn } from "react-hook-form"
import RefundAmountForm from ".."
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { getDefaultClaimValues } from "../../../details/utils/get-default-values"
describe("RefundAmountForm refund claim", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const order = fixtures.get("order") as unknown as Order
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: getDefaultClaimValues(order),
})
)
form = result.current
renderWithProviders(
<RefundAmountForm
form={nestedForm(form, "refund_amount")}
order={order}
/>
)
})
it("should render correctly", async () => {
// Initial value should be 0
expect(screen.getByText("€0.00")).toBeInTheDocument()
})
it("should update value when input is changed", async () => {
const button = screen.getByLabelText("Edit refund amount")
const user = userEvent.setup()
await user.click(button)
const input = screen.getByPlaceholderText("-")
fireEvent.change(input, { target: { value: "100" } })
await waitFor(() => {
const {
refund_amount: { amount },
} = form.getValues()
// We enter 100, but the value should be 10000 since we are transforming from dollars to cents
expect(amount).toEqual(10000)
})
})
})
@@ -1,72 +0,0 @@
import { Order, ShippingOption } from "@medusajs/medusa"
import { renderHook, screen } from "@testing-library/react"
import { useForm } from "react-hook-form"
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { ClaimSummary } from "../claim-summary"
describe("ClaimSummary", () => {
let order: Order
let so: ShippingOption
beforeEach(() => {
order = fixtures.get("order") as unknown as Order
so = fixtures.get("shipping_option") as unknown as ShippingOption
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: {
return_items: {
items: fixtures.get("order").items.map((item) => ({
item_id: item.id,
quantity: item.quantity,
return: true,
refundable: 90000,
total: 90000,
original_quantity: item.quantity,
})),
},
additional_items: {
items: fixtures.list("line_item", 5).map((item) => ({
item_id: item.id,
quantity: item.quantity,
price: 10000,
})),
},
replacement_shipping: {
option: {
label: so.name,
value: {
id: so.id,
taxRate: 0,
},
},
},
return_shipping: {
option: {
label: so.name,
value: {
id: so.id,
taxRate: 0,
},
},
},
claim_type: {
type: "replace",
},
},
})
)
renderWithProviders(<ClaimSummary order={order} form={result.current} />)
})
it("should render both a return and replacement shipping option", async () => {
expect(screen.getAllByText(so.name)).toHaveLength(2)
expect(screen.getByText("Return shipping")).toBeInTheDocument()
expect(screen.getByText("Replacement shipping")).toBeInTheDocument()
expect(screen.getAllByText("Free")).toHaveLength(2)
})
})
@@ -1,52 +0,0 @@
import { renderHook, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm, UseFormReturn } from "react-hook-form"
import SendNotificationForm from ".."
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
describe("SendNotificationForm", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: {
notification: {
send_notification: true,
},
},
})
)
form = result.current
renderWithProviders(
<SendNotificationForm
type="claim"
form={nestedForm(form, "notification")}
/>
)
})
it("should render initial value correctly", async () => {
const checkbox = screen.getByRole("checkbox")
expect(checkbox).toBeChecked()
})
it("should update the value when the checkbox is clicked", async () => {
const checkbox = screen.getByRole("checkbox")
const user = userEvent.setup()
await user.click(checkbox)
const {
notification: { send_notification },
} = form.getValues()
expect(send_notification).toEqual(false)
expect(checkbox).not.toBeChecked()
})
})
@@ -1,53 +0,0 @@
import { Order } from "@medusajs/medusa"
import { renderHook, screen, waitFor } from "@testing-library/react"
import { useForm, UseFormReturn } from "react-hook-form"
import ShippingAddressForm from ".."
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { getDefaultClaimValues } from "../../../details/utils/get-default-values"
describe("ShippingAddressForm with RegisterClaimMenu", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const order = fixtures.get("order") as unknown as Order
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: getDefaultClaimValues(order),
})
)
form = result.current
renderWithProviders(
<ShippingAddressForm
form={nestedForm(form, "shipping_address")}
order={order}
/>
)
})
it("should render the initial address correctly", async () => {
expect(screen.getByText("Shipping address")).toBeInTheDocument()
expect(screen.getByText("Faker Street 1, 3 Floor")).toBeInTheDocument()
expect(screen.getByText("Medusa JS, 2100 Copenhagen")).toBeInTheDocument()
expect(screen.getByText("Denmark")).toBeInTheDocument()
})
it("should render the address correctly when the address is changed", async () => {
await waitFor(() => {
form.setValue("shipping_address.address_1", "123 Second St")
form.setValue("shipping_address.address_2", "Apt 2")
})
const {
shipping_address: { address_1, address_2 },
} = form.getValues()
expect(address_1).toEqual("123 Second St")
expect(address_2).toEqual("Apt 2")
})
})
@@ -1,148 +0,0 @@
import { Order, ShippingOption } from "@medusajs/medusa"
import { renderHook, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"
import { useForm, UseFormReturn } from "react-hook-form"
import ShippingForm from ".."
import { fixtures } from "../../../../../../test/fixtures"
import { renderWithProviders } from "../../../../../../test/utils/render-with-providers"
import { nestedForm } from "../../../../../utils/nested-form"
import { CreateClaimFormType } from "../../../details/claim/register-claim-menu"
import { getDefaultClaimValues } from "../../../details/utils/get-default-values"
const selectFirstOption = async (user: UserEvent) => {
const combobox = screen.getByRole("combobox")
await waitFor(() => {
combobox.focus()
})
// Open dropdown
await user.keyboard("{arrowdown}")
// Go to first option and select
await user.keyboard("{arrowdown}")
await user.keyboard("{Enter}")
}
describe("ShippingForm return shipping", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const order = fixtures.get("order") as unknown as Order
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: getDefaultClaimValues(order),
})
)
form = result.current
renderWithProviders(
<div>
<ShippingForm
order={order}
isClaim
isReturn
form={nestedForm(form, "return_shipping")}
/>
</div>
)
})
it("should render correctly when type is refund", async () => {
expect(screen.getByText("Shipping for return items"))
expect(screen.queryByText("Shipping for replacement items")).toBeNull()
})
it("should render options when dropdown is opened", async () => {
const user = userEvent.setup()
const combobox = screen.getByRole("combobox")
await waitFor(() => {
combobox.focus()
})
await user.keyboard("{arrowdown}")
await waitFor(() => {
expect(screen.getAllByText("Free Shipping")).toHaveLength(5)
})
})
it("should select an option when clicked", async () => {
const user = userEvent.setup()
await selectFirstOption(user)
await waitFor(() => {
expect(screen.getAllByText("Free Shipping")).toHaveLength(1)
})
const { return_shipping } = form.getValues()
expect(return_shipping.option?.label).toEqual("Free Shipping")
expect(return_shipping.option?.value).toEqual(
expect.objectContaining({
id: expect.any(String),
taxRate: 0,
})
)
})
it("should render correctly when option is selected", async () => {
const shippingOption = fixtures.get(
"shipping_option"
) as unknown as ShippingOption
await waitFor(() => {
form.setValue("return_shipping.option", {
label: shippingOption.name,
value: {
id: shippingOption.id,
taxRate: 0.12,
},
})
})
await waitFor(() => {
expect(screen.getByText(shippingOption.name)).toBeInTheDocument()
})
})
})
describe("ShippingForm return shipping", () => {
let form: UseFormReturn<CreateClaimFormType, any>
beforeEach(() => {
const order = fixtures.get("order") as unknown as Order
const { result } = renderHook(() =>
useForm<CreateClaimFormType>({
defaultValues: {
...getDefaultClaimValues(order),
claim_type: {
type: "replace",
},
},
})
)
form = result.current
renderWithProviders(
<div>
<ShippingForm
order={order}
isClaim
form={nestedForm(form, "replacement_shipping")}
/>
</div>
)
})
it("should render correctly when type is replace", async () => {
expect(screen.getByText("Shipping for replacement items"))
expect(screen.queryByText("Shipping for return items")).toBeNull()
})
})
@@ -3,8 +3,8 @@ import {
Order,
VariantInventory,
} from "@medusajs/medusa"
import { DisplayTotal, PaymentDetails } from "../templates"
import React, { useContext, useMemo } from "react"
import { DisplayTotal, PaymentDetails } from "../templates"
import { ActionType } from "../../../../components/molecules/actionables"
import Badge from "../../../../components/fundamentals/badge"
@@ -15,11 +15,11 @@ import OrderLine from "../order-line"
import { ReservationItemDTO } from "@medusajs/types"
import ReserveItemsModal from "../reservation/reserve-items-modal"
import { Response } from "@medusajs/medusa-js"
import StatusIndicator from "../../../../components/fundamentals/status-indicator"
import { sum } from "lodash"
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
import { useMedusa } from "medusa-react"
import StatusIndicator from "../../../../components/fundamentals/status-indicator"
import useToggleState from "../../../../hooks/use-toggle-state"
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
type SummaryCardProps = {
order: Order
@@ -168,7 +168,7 @@ const SummaryCard: React.FC<SummaryCardProps> = ({ order, reservations }) => {
return (
<BodyCard
className={"mb-4 h-auto min-h-0 w-full"}
className={"h-auto min-h-0 w-full"}
title="Summary"
status={
isFeatureEnabled("inventoryService") &&
@@ -31,8 +31,10 @@ import { useEffect, useMemo, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import Spacer from "../../../components/atoms/spacer"
import Spinner from "../../../components/atoms/spinner"
import Tooltip from "../../../components/atoms/tooltip"
import WidgetContainer from "../../../components/extensions/widget-container"
import Button from "../../../components/fundamentals/button"
import DetailsIcon from "../../../components/fundamentals/details-icon"
import CancelIcon from "../../../components/fundamentals/icons/cancel-icon"
@@ -54,6 +56,7 @@ import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import { useWidgets } from "../../../providers/widget-provider"
import { isoAlpha2Countries } from "../../../utils/countries"
import { getErrorMessage } from "../../../utils/error-messages"
import extractCustomerName from "../../../utils/extract-customer-name"
@@ -196,6 +199,8 @@ const OrderDetails = () => {
useHotkeys("esc", () => navigate("/a/orders"))
useHotkeys("command+i", handleCopy)
const { getWidgets } = useWidgets()
const handleDeleteOrder = async () => {
const shouldDelete = await dialog({
heading: "Cancel order",
@@ -296,10 +301,22 @@ const OrderDetails = () => {
</BodyCard>
) : (
<>
<div>
{getWidgets("order.details.before").map((widget, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"order.details.before"}
widget={widget}
entity={order}
/>
)
})}
</div>
<div className="flex space-x-4">
<div className="flex h-full w-7/12 flex-col">
<div className="gap-y-base flex h-full w-7/12 flex-col">
<BodyCard
className={"mb-4 min-h-[200px] w-full"}
className={"min-h-[200px] w-full"}
customHeader={
<Tooltip side="top" content={"Copy ID"}>
<button
@@ -359,7 +376,7 @@ const OrderDetails = () => {
<SummaryCard order={order} reservations={reservations || []} />
<BodyCard
className={"mb-4 h-auto min-h-0 w-full"}
className={"h-auto min-h-0 w-full"}
title="Payment"
status={
<PaymentStatusComponent status={order.payment_status} />
@@ -428,7 +445,7 @@ const OrderDetails = () => {
</div>
</BodyCard>
<BodyCard
className={"mb-4 h-auto min-h-0 w-full"}
className={"h-auto min-h-0 w-full"}
title="Fulfillment"
status={
<FulfillmentStatusComponent
@@ -475,7 +492,7 @@ const OrderDetails = () => {
</div>
</BodyCard>
<BodyCard
className={"mb-4 h-auto min-h-0 w-full"}
className={"h-auto min-h-0 w-full"}
title="Customer"
actionables={customerActionables}
>
@@ -525,9 +542,20 @@ const OrderDetails = () => {
</div>
</div>
</BodyCard>
<div className="mt-large">
<RawJSON data={order} title="Raw order" />
<div>
{getWidgets("order.details.after").map((widget, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"order.details.after"}
widget={widget}
entity={order}
/>
)
})}
</div>
<RawJSON data={order} title="Raw order" />
<Spacer />
</div>
<Timeline orderId={order.id} />
</div>
@@ -13,6 +13,7 @@ import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import CopyToClipboard from "../../../components/atoms/copy-to-clipboard"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import Button from "../../../components/fundamentals/button"
import DetailsIcon from "../../../components/fundamentals/details-icon"
import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-icon"
@@ -24,6 +25,7 @@ import ConfirmationPrompt from "../../../components/organisms/confirmation-promp
import DeletePrompt from "../../../components/organisms/delete-prompt"
import { AddressType } from "../../../components/templates/address-form"
import useNotification from "../../../hooks/use-notification"
import { useWidgets } from "../../../providers/widget-provider"
import { isoAlpha2Countries } from "../../../utils/countries"
import { getErrorMessage } from "../../../utils/error-messages"
import extractCustomerName from "../../../utils/extract-customer-name"
@@ -119,6 +121,11 @@ const DraftOrderDetails = () => {
})
}
const { getWidgets } = useWidgets()
const afterWidgets = getWidgets("draft_order.details.after")
const beforeWidgets = getWidgets("draft_order.details.before")
const { cart } = draft_order || {}
const { region } = cart || {}
@@ -136,6 +143,21 @@ const DraftOrderDetails = () => {
) : (
<div className="flex space-x-4">
<div className="flex h-full w-full flex-col">
{beforeWidgets?.length > 0 && (
<div className="mb-4 flex w-full flex-col gap-y-4">
{beforeWidgets.map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone="draft_order.details.before"
entity={draft_order}
/>
)
})}
</div>
)}
<BodyCard
className={"mb-4 min-h-[200px] w-full"}
title={`Order #${draft_order.display_id}`}
@@ -359,6 +381,20 @@ const DraftOrderDetails = () => {
</div>
</div>
</BodyCard>
{afterWidgets?.length > 0 && (
<div className="mb-4 flex w-full flex-col gap-y-4">
{afterWidgets.map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone="draft_order.details.after"
entity={draft_order}
/>
)
})}
</div>
)}
<BodyCard
className={"mb-4 h-auto min-h-0 w-full pt-[15px]"}
title="Raw Draft Order"
@@ -1,11 +1,13 @@
import { useMemo, useState } from "react"
import { Route, Routes, useNavigate } from "react-router-dom"
import Spacer from "../../../components/atoms/spacer"
import WidgetContainer from "../../../components/extensions/widget-container"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../../components/organisms/body-card"
import TableViewHeader from "../../../components/organisms/custom-table-header"
import DraftOrderTable from "../../../components/templates/draft-order-table"
import { useWidgets } from "../../../providers/widget-provider"
import NewOrderFormProvider from "../new/form"
import NewOrder from "../new/new-order"
import DraftOrderDetails from "./details"
@@ -18,6 +20,8 @@ const DraftOrderIndex = () => {
const view = "drafts"
const [showNewOrder, setShowNewOrder] = useState(false)
const { getWidgets } = useWidgets()
const actions = useMemo(() => {
return [
{
@@ -29,7 +33,17 @@ const DraftOrderIndex = () => {
}, [view])
return (
<div className="flex h-full grow flex-col">
<div className="gap-y-xsmall flex h-full grow flex-col">
{getWidgets("draft_order.list.before").map((Widget, i) => {
return (
<WidgetContainer
key={i}
entity={null}
injectionZone="draft_order.list.before"
widget={Widget}
/>
)
})}
<div className="flex w-full grow flex-col">
<BodyCard
customHeader={
@@ -48,8 +62,18 @@ const DraftOrderIndex = () => {
>
<DraftOrderTable />
</BodyCard>
<Spacer />
</div>
{getWidgets("draft_order.list.after").map((Widget, i) => {
return (
<WidgetContainer
key={i}
entity={null}
injectionZone="draft_order.list.after"
widget={Widget}
/>
)
})}
<Spacer />
{showNewOrder && (
<NewOrderFormProvider>
<NewOrder onDismiss={() => setShowNewOrder(false)} />
@@ -3,6 +3,8 @@ import { Route, Routes, useNavigate } from "react-router-dom"
import { useAdminCreateBatchJob } from "medusa-react"
import Spacer from "../../components/atoms/spacer"
import RouteContainer from "../../components/extensions/route-container"
import WidgetContainer from "../../components/extensions/widget-container"
import Button from "../../components/fundamentals/button"
import ExportIcon from "../../components/fundamentals/icons/export-icon"
import BodyCard from "../../components/organisms/body-card"
@@ -12,6 +14,8 @@ import OrderTable from "../../components/templates/order-table"
import useNotification from "../../hooks/use-notification"
import useToggleState from "../../hooks/use-toggle-state"
import { usePolling } from "../../providers/polling-provider"
import { useRoutes } from "../../providers/route-provider"
import { useWidgets } from "../../providers/widget-provider"
import { getErrorMessage } from "../../utils/error-messages"
import Details from "./details"
import { transformFiltersAsExportContext } from "./utils"
@@ -35,6 +39,8 @@ const OrderIndex = () => {
state: exportModalOpen,
} = useToggleState(false)
const { getWidgets } = useWidgets()
const actions = useMemo(() => {
return [
<Button
@@ -73,7 +79,17 @@ const OrderIndex = () => {
return (
<>
<div className="flex h-full grow flex-col">
<div className="gap-y-xsmall flex h-full grow flex-col">
{getWidgets("order.list.before").map((w, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"order.list.before"}
widget={w}
entity={undefined}
/>
)
})}
<div className="flex w-full grow flex-col">
<BodyCard
customHeader={
@@ -92,8 +108,18 @@ const OrderIndex = () => {
>
<OrderTable setContextFilters={setContextFilters} />
</BodyCard>
<Spacer />
</div>
{getWidgets("order.list.after").map((w, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"order.list.after"}
widget={w}
entity={undefined}
/>
)
})}
<Spacer />
</div>
{exportModalOpen && (
<ExportModal
@@ -108,10 +134,23 @@ const OrderIndex = () => {
}
const Orders = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/products")
return (
<Routes>
<Route index element={<OrderIndex />} />
<Route path="/:id" element={<Details />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/orders"} />}
/>
)
})}
</Routes>
)
}
@@ -171,6 +171,10 @@ function ImportPrices(props: ImportPricesProps) {
}
}
const templateLink = process.env.ADMIN_PATH
? `${process.env.ADMIN_PATH}/temp/price-list-import-template.csv`
: `/temp/price-list-import-template.csv`
return (
<UploadModal
type="prices"
@@ -186,7 +190,7 @@ function ImportPrices(props: ImportPricesProps) {
summary={getSummary()}
onFileRemove={onFileRemove}
processUpload={processUpload}
templateLink="/temp/price-list-import-template.csv"
templateLink={templateLink}
/>
)
}
@@ -1,7 +1,11 @@
import { useAdminPriceList } from "medusa-react"
import { useParams } from "react-router-dom"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import RawJSON from "../../../components/organisms/raw-json"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorStatus } from "../../../utils/get-error-status"
import { mapPriceListToFormValues } from "../pricing-form/form/mappers"
import { PriceListFormProvider } from "../pricing-form/form/pricing-form-context"
import Header from "./sections/header"
@@ -9,8 +13,33 @@ import PricesDetails from "./sections/prices-details"
const PricingDetails = () => {
const { id } = useParams()
const navigate = useNavigate()
const { price_list, isLoading } = useAdminPriceList(id!)
const { price_list, isLoading, error } = useAdminPriceList(id!)
const { getWidgets } = useWidgets()
if (error) {
const errorStatus = getErrorStatus(error)
if (errorStatus) {
// If the product is not found, redirect to the 404 page
if (errorStatus.status === 404) {
navigate("/404")
return null
}
}
// Let the error boundary handle the error
throw error
}
if (isLoading || !price_list) {
return (
<div className="flex h-[calc(100vh-64px)] w-full items-center justify-center">
<Spinner variant="secondary" />
</div>
)
}
return (
<div className="pb-large">
@@ -19,18 +48,37 @@ const PricingDetails = () => {
path="/a/pricing"
className="mb-xsmall"
/>
<PriceListFormProvider priceList={mapPriceListToFormValues(price_list)}>
<div className="gap-y-xsmall flex flex-col">
{getWidgets("price_list.details.before").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={price_list}
injectionZone="price_list.details.before"
widget={w}
/>
)
})}
{!isLoading && price_list ? (
<PriceListFormProvider priceList={mapPriceListToFormValues(price_list)}>
<div className="gap-y-xsmall flex flex-col">
<Header priceList={price_list} />
<Header priceList={price_list} />
<PricesDetails id={price_list?.id} />
<PricesDetails id={price_list?.id} />
<RawJSON data={price_list} title="Raw price list" />
</div>
</PriceListFormProvider>
) : null}
{getWidgets("price_list.details.after").map((w, i) => {
return (
<WidgetContainer
key={i}
entity={price_list}
injectionZone="price_list.details.after"
widget={w}
/>
)
})}
<RawJSON data={price_list} title="Raw price list" />
</div>
</PriceListFormProvider>
</div>
)
}
@@ -1,8 +1,12 @@
import { Route, Routes, useNavigate } from "react-router-dom"
import Spacer from "../../components/atoms/spacer"
import RouteContainer from "../../components/extensions/route-container"
import WidgetContainer from "../../components/extensions/widget-container"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import BodyCard from "../../components/organisms/body-card"
import TableViewHeader from "../../components/organisms/custom-table-header"
import { useRoutes } from "../../providers/route-provider"
import { useWidgets } from "../../providers/widget-provider"
import PricingDetails from "./details"
import New from "./new"
import PricingTable from "./pricing-table"
@@ -18,8 +22,20 @@ const PricingIndex = () => {
},
]
const { getWidgets } = useWidgets()
return (
<div className="flex h-full flex-col">
<div className="gap-y-xsmall flex h-full flex-col">
{getWidgets("price_list.list.before").map((w, index) => {
return (
<WidgetContainer
key={index}
widget={w}
entity={null}
injectionZone="price_list.list.before"
/>
)
})}
<div className="flex w-full grow flex-col">
<BodyCard
actionables={actionables}
@@ -35,11 +51,24 @@ const PricingIndex = () => {
}
const Pricing = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/pricing")
return (
<Routes>
<Route index element={<PricingIndex />} />
<Route path="/new" element={<New />} />
<Route path="/:id" element={<PricingDetails />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/pricing"} />}
/>
)
})}
</Routes>
)
}
@@ -1,11 +1,28 @@
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../components/extensions/route-container"
import { useRoutes } from "../../providers/route-provider"
import ProductCategoryIndex from "./pages"
const ProductCategories = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/product-categories")
return (
<Routes>
<Route index element={<ProductCategoryIndex />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={
<RouteContainer route={r} previousPath={"/product-categories"} />
}
/>
)
})}
</Routes>
)
}
@@ -177,6 +177,10 @@ function ImportProducts(props: ImportProductsProps) {
}
}
const templateLink = process.env.ADMIN_PATH
? `${process.env.ADMIN_PATH}/temp/product-import-template.csv`
: "/temp/product-import-template.csv"
return (
<UploadModal
type="products"
@@ -190,7 +194,7 @@ function ImportProducts(props: ImportProductsProps) {
onFileRemove={onFileRemove}
processUpload={processUpload}
fileTitle={"products list"}
templateLink="/temp/product-import-template.csv"
templateLink={templateLink}
errorMessage={batchJob?.result?.errors?.join(" \n")}
description2Title="Unsure about how to arrange your list?"
description2Text="Download the template below to ensure you are following the correct format."
@@ -2,18 +2,22 @@ import { useAdminProduct } from "medusa-react"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
import ProductAttributesSection from "../../../components/organisms/product-attributes-section"
import ProductGeneralSection from "../../../components/organisms/product-general-section"
import ProductMediaSection from "../../../components/organisms/product-media-section"
import ProductRawSection from "../../../components/organisms/product-raw-section"
import ProductThumbnailSection from "../../../components/organisms/product-thumbnail-section"
import ProductVariantsSection from "../../../components/organisms/product-variants-section"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorStatus } from "../../../utils/get-error-status"
const Edit = () => {
const { id } = useParams()
const navigate = useNavigate()
const { getWidgets } = useWidgets()
const { product, status, error } = useAdminProduct(id || "")
if (error) {
@@ -32,7 +36,6 @@ const Edit = () => {
}
if (status === "loading" || !product) {
// temp, perhaps use skeletons?
return (
<div className="flex h-[calc(100vh-64px)] w-full items-center justify-center">
<Spinner variant="secondary" />
@@ -47,16 +50,38 @@ const Edit = () => {
label="Back to Products"
className="mb-xsmall"
/>
<div className="gap-x-base grid grid-cols-12">
<div className="gap-y-xsmall col-span-8 flex flex-col">
<ProductGeneralSection product={product} />
<ProductVariantsSection product={product} />
<ProductAttributesSection product={product} />
<ProductRawSection product={product} />
</div>
<div className="gap-y-xsmall col-span-4 flex flex-col">
<ProductThumbnailSection product={product} />
<ProductMediaSection product={product} />
<div className="gap-y-xsmall flex flex-col">
{getWidgets("product.details.before").map((w, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"product.details.before"}
widget={w}
entity={product}
/>
)
})}
<div className="gap-x-base grid grid-cols-12">
<div className="gap-y-xsmall col-span-8 flex flex-col">
<ProductGeneralSection product={product} />
<ProductVariantsSection product={product} />
<ProductAttributesSection product={product} />
{getWidgets("product.details.after").map((w, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"product.details.after"}
widget={w}
entity={product}
/>
)
})}
<ProductRawSection product={product} />
</div>
<div className="gap-y-xsmall col-span-4 flex flex-col">
<ProductThumbnailSection product={product} />
<ProductMediaSection product={product} />
</div>
</div>
</div>
</div>
@@ -1,12 +1,27 @@
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../components/extensions/route-container"
import { useRoutes } from "../../providers/route-provider"
import Edit from "./edit"
import Overview from "./overview"
const ProductsRoute = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/products")
return (
<Routes>
<Route index element={<Overview />} />
<Route path="/:id" element={<Edit />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={"/products"} />}
/>
)
})}
</Routes>
)
}
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import Fade from "../../../components/atoms/fade-wrapper"
import Spacer from "../../../components/atoms/spacer"
import WidgetContainer from "../../../components/extensions/widget-container"
import Button from "../../../components/fundamentals/button"
import ExportIcon from "../../../components/fundamentals/icons/export-icon"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
@@ -16,6 +17,7 @@ import ProductTable from "../../../components/templates/product-table"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
import { usePolling } from "../../../providers/polling-provider"
import { useWidgets } from "../../../providers/widget-provider"
import { getErrorMessage } from "../../../utils/error-messages"
import ImportProducts from "../batch-job/import"
import NewProduct from "../new"
@@ -30,7 +32,10 @@ const Overview = () => {
state: createProductState,
close: closeProductCreate,
open: openProductCreate,
} = useToggleState()
} = useToggleState(
!location.search.includes("view=collections") &&
location.search.includes("modal=new")
)
const { resetInterval } = usePolling()
const createBatchJob = useAdminCreateBatchJob()
@@ -39,6 +44,8 @@ const Overview = () => {
const createCollection = useAdminCreateCollection()
const { getWidgets } = useWidgets()
useEffect(() => {
if (location.search.includes("?view=collections")) {
setView("collections")
@@ -110,13 +117,19 @@ const Overview = () => {
open: openExportModal,
close: closeExportModal,
state: exportModalOpen,
} = useToggleState(false)
} = useToggleState(
!location.search.includes("view=collections") &&
location.search.includes("modal=export")
)
const {
open: openImportModal,
close: closeImportModal,
state: importModalOpen,
} = useToggleState(false)
} = useToggleState(
!location.search.includes("view=collections") &&
location.search.includes("modal=import")
)
const handleCreateCollection = async (data, colMetadata) => {
const metadata = colMetadata
@@ -163,7 +176,17 @@ const Overview = () => {
return (
<>
<div className="flex h-full grow flex-col">
<div className="gap-y-xsmall flex h-full grow flex-col">
{getWidgets("product.list.before").map((w, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"product.list.before"}
widget={w}
entity={undefined}
/>
)
})}
<div className="flex w-full grow flex-col">
<BodyCard
forceDropdown={false}
@@ -181,6 +204,16 @@ const Overview = () => {
</BodyCard>
<Spacer />
</div>
{getWidgets("product.list.after").map((w, i) => {
return (
<WidgetContainer
key={i}
injectionZone={"product.list.after"}
widget={w}
entity={undefined}
/>
)
})}
</div>
{showNewCollection && (
@@ -1,11 +1,31 @@
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../components/extensions/route-container"
import { useRoutes } from "../../providers/route-provider"
import Index from "./pages"
const PublishableApiKeysRoute = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/publishable-api-keys")
return (
<Routes>
<Route index element={<Index />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={
<RouteContainer
route={r}
previousPath={"/publishable-api-keys"}
/>
}
/>
)
})}
</Routes>
)
}
@@ -1,11 +1,28 @@
import Details from "./pages/details"
import { Route, Routes } from "react-router-dom"
import RouteContainer from "../../components/extensions/route-container"
import { useRoutes } from "../../providers/route-provider"
import Details from "./pages/details"
const SalesChannels = () => {
const { getNestedRoutes } = useRoutes()
const nestedRoutes = getNestedRoutes("/sales-channels")
return (
<Routes>
<Route index element={<Details />} />
<Route path="/:id" element={<Details />} />
{nestedRoutes.map((r, i) => {
return (
<Route
path={r.path}
key={i}
element={
<RouteContainer route={r} previousPath={"/sales-channels"} />
}
/>
)
})}
</Routes>
)
}
@@ -1,18 +1,21 @@
import React from "react"
import { Route, Routes } from "react-router-dom"
import SettingsCard from "../../components/atoms/settings-card"
import Spacer from "../../components/atoms/spacer"
import SettingContainer from "../../components/extensions/setting-container"
import SettingsPageErrorElement from "../../components/extensions/setting-container/setting-error-element"
import FeatureToggle from "../../components/fundamentals/feature-toggle"
import ArrowUTurnLeft from "../../components/fundamentals/icons/arrow-uturn-left"
import ChannelsIcon from "../../components/fundamentals/icons/channels-icon"
import CoinsIcon from "../../components/fundamentals/icons/coins-icon"
import CrosshairIcon from "../../components/fundamentals/icons/crosshair-icon"
import DollarSignIcon from "../../components/fundamentals/icons/dollar-sign-icon"
import GearIcon from "../../components/fundamentals/icons/gear-icon"
import HappyIcon from "../../components/fundamentals/icons/happy-icon"
import KeyIcon from "../../components/fundamentals/icons/key-icon"
import MailIcon from "../../components/fundamentals/icons/mail-icon"
import MapPinIcon from "../../components/fundamentals/icons/map-pin-icon"
import TaxesIcon from "../../components/fundamentals/icons/taxes-icon"
import TruckIcon from "../../components/fundamentals/icons/truck-icon"
import UsersIcon from "../../components/fundamentals/icons/users-icon"
import SettingsOverview from "../../components/templates/settings-overview"
import { useSettings } from "../../providers/setting-provider"
import CurrencySettings from "./currencies"
import Details from "./details"
import PersonalInformation from "./personal-information"
@@ -21,95 +24,164 @@ import ReturnReasons from "./return-reasons"
import Taxes from "./taxes"
import Users from "./users"
type SettingsCardType = {
heading: string
description: string
icon?: React.ComponentType
to: string
feature_flag?: string
}
const settings: SettingsCardType[] = [
{
heading: "API Key Management",
description: "Create and manage API keys",
icon: KeyIcon,
to: "/a/publishable-api-keys",
feature_flag: "publishable_api_keys",
},
{
heading: "Currencies",
description: "Manage the currencies of your store",
icon: CoinsIcon,
to: "/a/settings/currencies",
},
{
heading: "Personal Information",
description: "Manage your Medusa profile",
icon: HappyIcon,
to: "/a/settings/personal-information",
},
{
heading: "Regions",
description: "Manage shipping, payment, and fulfillment across regions",
icon: MapPinIcon,
to: "/a/settings/regions",
},
{
heading: "Return Reasons",
description: "Manage resons for returned items",
icon: ArrowUTurnLeft,
to: "/a/settings/return-reasons",
},
{
heading: "Sales Channels",
description: "Control which product are available in which channels",
icon: ChannelsIcon,
feature_flag: "sales_channels",
to: "/a/sales-channels",
},
{
heading: "Store Details",
description: "Manage your business details",
icon: CrosshairIcon,
to: "/a/settings/details",
},
{
heading: "Taxes",
description: "Manage taxes across regions and products",
icon: TaxesIcon,
to: "/a/settings/taxes",
},
{
heading: "The Team",
description: "Manage users of your Medusa Store",
icon: UsersIcon,
to: "/a/settings/team",
},
]
const renderCard = ({
heading,
description,
icon,
to,
feature_flag,
}: SettingsCardType) => {
const Icon = icon || GearIcon
const card = (
<SettingsCard
heading={heading}
description={description}
icon={<Icon />}
to={to}
/>
)
if (feature_flag) {
return <FeatureToggle featureFlag={feature_flag}>{card}</FeatureToggle>
}
return card
}
const SettingsIndex = () => {
const { getCards } = useSettings()
const extensionCards = getCards()
return (
<SettingsOverview>
<SettingsCard
heading={"Regions"}
description={"Manage the markets you will operate within"}
icon={<MapPinIcon />}
to={`/a/settings/regions`}
/>
<SettingsCard
heading={"Currencies"}
description={"Manage the markets you will operate within"}
icon={<CoinsIcon />}
to={`/a/settings/currencies`}
/>
<SettingsCard
heading={"Store Details"}
description={"Manage your business details"}
icon={<CrosshairIcon />}
to={`/a/settings/details`}
/>
<SettingsCard
heading={"Shipping"}
description={"Manage shipping profiles"}
icon={<TruckIcon />}
to={`/a/settings/shipping-profiles`}
disabled={true}
/>
<SettingsCard
heading={"Return Reasons"}
description={"Manage Order settings"}
icon={<DollarSignIcon />}
to={`/a/settings/return-reasons`}
/>
<SettingsCard
heading={"The Team"}
description={"Manage users of your Medusa Store"}
icon={<UsersIcon />}
to={`/a/settings/team`}
/>
<SettingsCard
heading={"Personal Information"}
description={"Manage your Medusa profile"}
icon={<HappyIcon />}
to={`/a/settings/personal-information`}
/>
<SettingsCard
heading={"hello@medusajs.com"}
description={"Cant find the answers youre looking for?"}
icon={<MailIcon />}
externalLink={"mailto: hello@medusajs.com"}
/>
<SettingsCard
heading={"Tax Settings"}
description={"Manage taxes across regions and products"}
icon={<TaxesIcon />}
to={`/a/settings/taxes`}
/>
<FeatureToggle featureFlag="sales_channels">
<SettingsCard
heading={"Sales channels"}
description={"Control which products are available in which channels"}
icon={<ChannelsIcon />}
to={`/a/sales-channels`}
/>
</FeatureToggle>
<FeatureToggle featureFlag="publishable_api_keys">
<SettingsCard
heading={"API key management"}
description={"Create and manage API keys"}
icon={<KeyIcon />}
to={`/a/publishable-api-keys`}
/>
</FeatureToggle>
</SettingsOverview>
<div className="gap-y-xlarge flex flex-col">
<div className="gap-y-large flex flex-col">
<div className="gap-y-2xsmall flex flex-col">
<h2 className="inter-xlarge-semibold">General</h2>
<p className="inter-base-regular text-grey-50">
Manage the general settings for your store
</p>
</div>
<div className="medium:grid-cols-2 gap-y-xsmall grid grid-cols-1 gap-x-4">
{settings.map((s) => renderCard(s))}
</div>
</div>
{extensionCards.length > 0 && (
<div className="gap-y-large flex flex-col">
<div className="gap-y-2xsmall flex flex-col">
<h2 className="inter-xlarge-semibold">Extensions</h2>
<p className="inter-base-regular text-grey-50">
Manage the settings for your store&apos;s extensions
</p>
</div>
<div className="medium:grid-cols-2 gap-y-xsmall grid grid-cols-1 gap-x-4">
{getCards().map((s) =>
renderCard({
heading: s.label,
description: s.description,
icon: s.icon,
to: `/a/settings${s.path}`,
})
)}
</div>
</div>
)}
<Spacer />
</div>
)
}
const Settings = () => (
<Routes>
<Route index element={<SettingsIndex />} />
<Route path="/details" element={<Details />} />
<Route path="/regions/*" element={<Regions />} />
<Route path="/currencies" element={<CurrencySettings />} />
<Route path="/return-reasons" element={<ReturnReasons />} />
<Route path="/team" element={<Users />} />
<Route path="/personal-information" element={<PersonalInformation />} />
<Route path="/taxes/*" element={<Taxes />} />
</Routes>
)
const Settings = () => {
const { getSettings } = useSettings()
return (
<Routes>
<Route index element={<SettingsIndex />} />
<Route path="/details" element={<Details />} />
<Route path="/regions/*" element={<Regions />} />
<Route path="/currencies" element={<CurrencySettings />} />
<Route path="/return-reasons" element={<ReturnReasons />} />
<Route path="/team" element={<Users />} />
<Route path="/personal-information" element={<PersonalInformation />} />
<Route path="/taxes/*" element={<Taxes />} />
{getSettings().map((s) => (
<Route
key={s.path}
path={s.path}
element={<SettingContainer Page={s.Page} />}
errorElement={<SettingsPageErrorElement origin={s.origin} />}
/>
))}
</Routes>
)
}
export default Settings