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:
committed by
GitHub
parent
26c78bbc03
commit
f1a05f4725
@@ -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">
|
||||
|
||||
-59
@@ -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")
|
||||
})
|
||||
})
|
||||
-59
@@ -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)
|
||||
})
|
||||
})
|
||||
-119
@@ -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)
|
||||
})
|
||||
})
|
||||
-80
@@ -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)
|
||||
})
|
||||
})
|
||||
-59
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
-72
@@ -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)
|
||||
})
|
||||
})
|
||||
-52
@@ -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()
|
||||
})
|
||||
})
|
||||
-53
@@ -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")
|
||||
})
|
||||
})
|
||||
-148
@@ -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={"Can’t find the answers you’re 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'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
|
||||
|
||||
Reference in New Issue
Block a user