diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index 055025a412..af1b57a3c0 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -6,6 +6,8 @@
"add": "Add",
"start": "Start",
"end": "End",
+ "open": "Open",
+ "close": "Close",
"apply": "Apply",
"range": "Range",
"search": "Search",
@@ -55,6 +57,7 @@
"cancel": "Cancel",
"save": "Save",
"continue": "Continue",
+ "confirm": "Confirm",
"edit": "Edit",
"download": "Download",
"clearAll": "Clear all",
@@ -257,6 +260,16 @@
"requiresAction": "Requires action"
}
},
+ "edits": {
+ "title": "Edit order",
+ "currentItems": "Current items",
+ "currentItemsDescription": "Adjust item quantity or remove.",
+ "addItemsDescription": "You can add new items to the order.",
+ "addItems": "Add items",
+ "amountPaid": "Amount paid",
+ "newTotal": "New total",
+ "differenceDue": "Difference due"
+ },
"reservations": {
"allocatedLabel": "Allocated",
"notAllocatedLabel": "Not allocated"
@@ -736,6 +749,7 @@
"availability": "Availability",
"inventory": "Inventory",
"optional": "Optional",
+ "note": "Note",
"taxInclusivePricing": "Tax inclusive pricing",
"taxRate": "Tax Rate",
"taxCode": "Tax Code",
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx
index 56a981054d..06714047ce 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx
@@ -6,14 +6,16 @@ type MoneyAmountCellProps = {
currencyCode: string
amount?: number | null
align?: "left" | "right"
+ className?: string
}
export const MoneyAmountCell = ({
currencyCode,
amount,
align = "left",
+ className,
}: MoneyAmountCellProps) => {
- if (!amount) {
+ if (typeof amount === "undefined" || amount === null) {
return
}
@@ -21,10 +23,14 @@ export const MoneyAmountCell = ({
return (
{formatted}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx
index 442cd2a6a2..5cd03c2614 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx
@@ -120,6 +120,10 @@ export const v1Routes: RouteObject[] = [
lazy: () =>
import("../../routes/orders/order-transfer-ownership"),
},
+ {
+ path: "edit",
+ lazy: () => import("../../routes/orders/order-edit"),
+ },
],
},
],
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
index ec6e68a607..8a3d09b182 100644
--- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
@@ -38,7 +38,7 @@ const Header = ({ order }: { order: Order }) => {
actions: [
{
label: t("orders.summary.editItems"),
- to: "#", // TODO: Open modal to edit items
+ to: `/orders/${order.id}/edit`,
icon: ,
},
{
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts
index bd7e8fe093..23e80b0a8e 100644
--- a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts
@@ -1,2 +1,2 @@
export const orderExpand =
- "items,items.variant,items.variant.options,sales_channel,shipping_methods,shipping_methods.shipping_option,discounts,payments,customer,shipping_address,shipping_address.country,billing_address,billing_address.country,fulfillments,fulfillments.items,fulfillments.items.item,fulfillments.tracking_links,refunds"
+ "items,items.variant,items.variant.options,sales_channel,shipping_methods,shipping_methods.shipping_option,discounts,payments,customer,shipping_address,shipping_address.country,billing_address,billing_address.country,fulfillments,fulfillments.items,fulfillments.items.item,fulfillments.tracking_links,refunds,edits,edits.items,edits.items.variant,edits.items.variant.product"
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx
new file mode 100644
index 0000000000..15b6e1faad
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx
@@ -0,0 +1 @@
+export * from "./order-edit-form"
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx
new file mode 100644
index 0000000000..48f9625a22
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx
@@ -0,0 +1,359 @@
+import React, { useEffect, useMemo, useState } from "react"
+import { useForm } from "react-hook-form"
+import * as zod from "zod"
+import { useTranslation } from "react-i18next"
+import { useSearchParams } from "react-router-dom"
+import {
+ adminOrderEditsKeys,
+ adminOrderKeys,
+ useAdminCancelOrderEdit,
+ useAdminConfirmOrderEdit,
+ useAdminOrderEditAddLineItem,
+ useAdminUpdateOrderEdit,
+} from "medusa-react"
+
+import { Button, clx, Heading, Text, Textarea } from "@medusajs/ui"
+import { Order, OrderEdit } from "@medusajs/medusa"
+
+import {
+ RouteFocusModal,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { SplitView } from "../../../../../components/layout/split-view"
+import { VariantTable } from "../variant-table"
+
+import { medusa, queryClient } from "../../../../../lib/medusa.ts"
+import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
+import { Form } from "../../../../../components/common/form"
+import { OrderEditItem } from "./order-edit-item"
+
+type OrderEditFormProps = {
+ order: Order
+ orderEdit: OrderEdit
+}
+
+const QuantitiesSchema = zod.union(
+ zod.record(zod.string(), zod.number().optional()),
+ zod.object({ note: zod.string().optional() })
+)
+
+export function OrderEditForm({ order, orderEdit }: OrderEditFormProps) {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+ const [, setSearchParams] = useSearchParams()
+
+ /**
+ * STATE
+ */
+ const [open, setOpen] = useState(false)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isAddingItems, setIsAddingItems] = useState(false)
+
+ /**
+ * FORM
+ */
+ const form = useForm>({
+ defaultValues: order.items.reduce(
+ (acc, i) => {
+ acc[i.id] = i.quantity
+ return acc
+ },
+ { note: orderEdit.internal_note || "" }
+ ),
+ })
+
+ /**
+ * CRUD HOOKS
+ */
+ const { mutateAsync: confirmOrderEdit } = useAdminConfirmOrderEdit(
+ orderEdit.id
+ )
+ const { mutateAsync: cancelOrderEdit } = useAdminCancelOrderEdit(orderEdit.id)
+ const { mutateAsync: addLineItemToOrderEdit } = useAdminOrderEditAddLineItem(
+ orderEdit.id
+ )
+ const { mutateAsync: updateOrderEdit } = useAdminUpdateOrderEdit(orderEdit.id)
+
+ const onQuantityChangeComplete = async (itemId: string) => {
+ setIsSubmitting(true)
+
+ const quantity = form.getValues()[itemId]
+
+ // if (form.getValues()[itemId] === 0) {
+ // await medusa.admin.orderEdits.removeLineItem(orderEdit.id, itemId)
+ // } else
+
+ if (quantity > 0) {
+ await medusa.admin.orderEdits.updateLineItem(orderEdit.id, itemId, {
+ quantity,
+ })
+ }
+ await queryClient.invalidateQueries(
+ adminOrderEditsKeys.detail(orderEdit.id)
+ )
+
+ setIsSubmitting(false)
+ }
+
+ const currentItems = useMemo(
+ () =>
+ orderEdit?.items
+ .sort((i1, i2) => i1.id.localeCompare(i2.id))
+ .filter((i) => i.original_item_id) || [],
+ [orderEdit]
+ )
+ const addedItems = useMemo(
+ () =>
+ orderEdit?.items
+ .sort((i1, i2) => i1.id.localeCompare(i2.id))
+ .filter((i) => !i.original_item_id) || [],
+ [orderEdit]
+ )
+
+ /**
+ * EFFECTS
+ */
+ useEffect(() => {
+ if (orderEdit) {
+ orderEdit?.items.forEach((i) => {
+ form.setValue(i.id, i.quantity)
+ })
+ }
+ }, [orderEdit?.items.length])
+
+ /**
+ * HANDLERS
+ */
+ const handleOpenChange = (open: boolean) => {
+ if (!open) {
+ setSearchParams(
+ {},
+ {
+ replace: true,
+ }
+ )
+ }
+
+ setOpen(open)
+ }
+
+ const onVariantsSelect = async (variantIds: string[]) => {
+ setIsAddingItems(true)
+ try {
+ await Promise.all(
+ variantIds.map((id) =>
+ addLineItemToOrderEdit({ variant_id: id, quantity: 1 })
+ )
+ )
+
+ await queryClient.invalidateQueries(adminOrderKeys.detail(order.id))
+ } finally {
+ setIsAddingItems(false)
+ }
+
+ setOpen(false)
+ }
+
+ const onItemRemove = async (itemId: string) => {
+ setIsSubmitting(true)
+
+ const change = orderEdit.changes.find(
+ (change) =>
+ change.line_item_id === itemId ||
+ change.original_line_item_id === itemId
+ )
+ try {
+ if (change) {
+ if (change.type === "item_add") {
+ await medusa.admin.orderEdits.deleteItemChange(
+ orderEdit.id,
+ change.id
+ )
+ }
+ if (change.type === "item_update") {
+ await medusa.admin.orderEdits.deleteItemChange(
+ orderEdit.id,
+ change.id
+ )
+ await medusa.admin.orderEdits.removeLineItem(orderEdit.id, itemId)
+ }
+ } else {
+ await medusa.admin.orderEdits.removeLineItem(orderEdit.id, itemId)
+ }
+
+ await queryClient.invalidateQueries(
+ adminOrderEditsKeys.detail(orderEdit.id)
+ )
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ setIsSubmitting(true)
+
+ try {
+ if (data.note !== orderEdit?.internal_note) {
+ await updateOrderEdit({ internal_note: data.note })
+ }
+
+ await confirmOrderEdit() // TODO error notification if fails
+ } finally {
+ setIsSubmitting(false)
+ }
+
+ handleSuccess(`/orders/${order.id}`)
+ })
+
+ // TODO pass on "cancel" close
+ const handlCancel = async () => {
+ await cancelOrderEdit()
+
+ handleSuccess(`/orders/${order.id}`)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-item.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-item.tsx
new file mode 100644
index 0000000000..84e2c20f97
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-item.tsx
@@ -0,0 +1,112 @@
+import React from "react"
+import { useTranslation } from "react-i18next"
+import { UseFormReturn } from "react-hook-form"
+
+import { Input, Text } from "@medusajs/ui"
+import { LineItem } from "@medusajs/medusa"
+
+import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
+import { Thumbnail } from "../../../../../components/common/thumbnail"
+import { Trash } from "@medusajs/icons"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import { Form } from "../../../../../components/common/form"
+
+type OrderEditItemProps = {
+ item: LineItem
+ currencyCode: string
+
+ form: UseFormReturn>
+ onQuantityChangeComplete: (id: string) => void
+ onRemove: (id: string) => void
+}
+
+function OrderEditItem({
+ item,
+ currencyCode,
+ form,
+ onQuantityChangeComplete,
+ onRemove,
+}: OrderEditItemProps) {
+ const { t } = useTranslation()
+
+ const thumbnail = item.thumbnail
+
+ return (
+
+
+
+
+
+
+
+ {item.title}
+
+ {item.variant.sku && (${item.variant.sku})}
+
+
+ {item.variant.title}
+
+
+
+
+
+
+
+
+
+
,
+ onClick: () => onRemove(item.id),
+ },
+ ],
+ },
+ ]}
+ />
+
+
+
+
+
+ {t("fields.quantity")}
+
+
+
{
+ return (
+
+
+ {
+ const val = e.target.value
+ field.onChange(val === "" ? null : Number(val))
+ }}
+ onBlur={() => {
+ if (typeof form.getValues()[item.id] === "undefined") {
+ field.onChange(1)
+ }
+ onQuantityChangeComplete(item.id)
+ }}
+ />
+
+
+
+ )
+ }}
+ />
+
+
+ )
+}
+
+export { OrderEditItem }
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/index.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/index.tsx
new file mode 100644
index 0000000000..bd4dd39a88
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/index.tsx
@@ -0,0 +1 @@
+export * from "./variant-table"
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-columns.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-columns.tsx
new file mode 100644
index 0000000000..f9586ffc6b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-columns.tsx
@@ -0,0 +1,97 @@
+import React, { useMemo } from "react"
+import { useTranslation } from "react-i18next"
+import { createColumnHelper } from "@tanstack/react-table"
+import { Checkbox } from "@medusajs/ui"
+import {
+ ProductCell,
+ ProductHeader,
+} from "../../../../../components/table/table-cells/product/product-cell"
+import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
+import { PricedVariant } from "@medusajs/client-types"
+
+const columnHelper = createColumnHelper()
+
+export const useVariantTableColumns = (currencyCode: string) => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.display({
+ id: "product",
+ header: () => ,
+ cell: ({ row }) => ,
+ }),
+ columnHelper.accessor("sku", {
+ header: t("fields.sku"),
+ }),
+ columnHelper.accessor("title", {
+ header: t("fields.variant"),
+ }),
+ columnHelper.display({
+ id: "amount",
+ header: () => (
+
+ {t("fields.price")}
+
+ ),
+ cell: ({ row: { original } }) => {
+ if (!original.original_price_incl_tax) {
+ return null
+ }
+
+ const showOriginal = original.calculated_price_type !== "default"
+
+ return (
+
+
+ {showOriginal && (
+
+
+
+ )}
+
+
+
+
+
+ )
+ },
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-filters.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-filters.tsx
new file mode 100644
index 0000000000..fe4d937a77
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-filters.tsx
@@ -0,0 +1,17 @@
+import { useTranslation } from "react-i18next"
+import { Filter } from "../../../../../components/table/data-table"
+
+export const useVariantTableFilters = () => {
+ const { t } = useTranslation()
+
+ const filters: Filter[] = [
+ { label: t("fields.createdAt"), key: "created_at" },
+ { label: t("fields.updatedAt"), key: "updated_at" },
+ ].map((f) => ({
+ key: f.key,
+ label: f.label,
+ type: "date",
+ }))
+
+ return filters
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-query.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-query.tsx
new file mode 100644
index 0000000000..b1e65cc8f4
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-query.tsx
@@ -0,0 +1,32 @@
+import { AdminGetVariantsParams } from "@medusajs/medusa"
+
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+export const useVariantTableQuery = ({
+ pageSize = 50,
+ prefix,
+}: {
+ pageSize?: number
+ prefix: string
+}) => {
+ const raw = useQueryParams(
+ ["offset", "q", "title", "customer_id", "inventory_quantity"],
+ prefix
+ )
+
+ const searchParams: AdminGetVariantsParams = {
+ limit: pageSize,
+ offset: raw.offset ? Number(raw.offset) : 0,
+ q: raw.q,
+ title: raw.title,
+ customer_id: raw.customer_id,
+ inventory_quantity: raw.inventory_quantity
+ ? Number(raw.inventory_quantity)
+ : undefined,
+ }
+
+ return {
+ searchParams,
+ raw,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/variant-table.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/variant-table.tsx
new file mode 100644
index 0000000000..4ab7d3ecdb
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/variant-table.tsx
@@ -0,0 +1,163 @@
+import { PricedVariant } from "@medusajs/client-types"
+import { useTranslation } from "react-i18next"
+import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
+import { useState } from "react"
+import { useAdminVariants } from "medusa-react"
+import { Button } from "@medusajs/ui"
+import { Order } from "@medusajs/medusa"
+
+import { DataTable } from "../../../../../components/table/data-table"
+import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
+import { SplitView } from "../../../../../components/layout/split-view"
+
+import { useVariantTableQuery } from "./use-variant-table-query"
+import { useVariantTableColumns } from "./use-variant-table-columns"
+import { useVariantTableFilters } from "./use-variant-table-filters"
+
+const PAGE_SIZE = 50
+
+export type Option = {
+ value: string
+ label: string
+}
+
+const Footer = ({
+ onSave,
+ isAddingItems,
+}: {
+ isAddingItems: boolean
+ onSave: () => void
+}) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+type VariantTableProps = {
+ onSave: (ids: string[]) => Promise
+ isAddingItems: boolean
+ order: Order
+}
+
+export const VariantTable = ({
+ onSave,
+ order,
+ isAddingItems,
+}: VariantTableProps) => {
+ const [rowSelection, setRowSelection] = useState({})
+ const [intermediate, setIntermediate] = useState([])
+
+ const { searchParams, raw } = useVariantTableQuery({
+ pageSize: PAGE_SIZE,
+ })
+
+ const { variants, count, isLoading, isError, error } = useAdminVariants(
+ {
+ ...searchParams,
+ region_id: order.region_id,
+ cart_id: order.cart_id,
+ customer_id: order.customer_id,
+ currency_code: order.currency_code,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (fn) => {
+ const newState: RowSelectionState =
+ typeof fn === "function" ? fn(rowSelection) : fn
+
+ const added = Object.keys(newState).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (added.length) {
+ const addedProducts = (variants?.filter((v) => added.includes(v.id!)) ??
+ []) as PricedVariant[]
+
+ if (addedProducts.length > 0) {
+ const newConditions = addedProducts.map((p) => p.id!)
+
+ setIntermediate((prev) => {
+ const filteredPrev = prev.filter((p) => newState[p])
+ return Array.from(new Set([...filteredPrev, ...newConditions]))
+ })
+ }
+
+ setRowSelection(newState)
+ }
+
+ const removed = Object.keys(rowSelection).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (removed.length) {
+ setIntermediate((prev) => {
+ return prev.filter((p) => !removed.includes(p))
+ })
+
+ setRowSelection(newState)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(intermediate)
+ }
+
+ const columns = useVariantTableColumns(order.currency_code)
+ const filters = useVariantTableFilters()
+
+ const { table } = useDataTable({
+ data: (variants ?? []) as PricedVariant[],
+ columns: columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ enableRowSelection: true,
+ rowSelection: {
+ state: rowSelection,
+ updater,
+ },
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-edit/index.ts
new file mode 100644
index 0000000000..691631e0c9
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/index.ts
@@ -0,0 +1,2 @@
+export { orderLoader as loader } from "./loader"
+export { OrderEdit as Component } from "./order-edit"
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/loader.ts b/packages/admin-next/dashboard/src/routes/orders/order-edit/loader.ts
new file mode 100644
index 0000000000..4fe946f48d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/loader.ts
@@ -0,0 +1,25 @@
+import { AdminOrdersRes } from "@medusajs/medusa"
+import { Response } from "@medusajs/medusa-js"
+import { adminOrderKeys } from "medusa-react"
+import { LoaderFunctionArgs } from "react-router-dom"
+
+import { medusa, queryClient } from "../../../lib/medusa"
+import { orderExpand } from "../order-detail/constants"
+
+const orderEditQuery = (id: string) => ({
+ queryKey: adminOrderKeys.detail(id),
+ queryFn: async () =>
+ medusa.admin.orders.retrieve(id, {
+ expand: orderExpand,
+ }),
+})
+
+export const orderLoader = async ({ params }: LoaderFunctionArgs) => {
+ const id = params.id
+ const query = orderEditQuery(id!)
+
+ return (
+ queryClient.getQueryData>(query.queryKey) ??
+ (await queryClient.fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/order-edit.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/order-edit.tsx
new file mode 100644
index 0000000000..aa58145230
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/order-edit.tsx
@@ -0,0 +1,77 @@
+import {
+ useAdminCreateOrderEdit,
+ useAdminOrder,
+ useAdminOrderEdit,
+} from "medusa-react"
+import { useLoaderData, useParams } from "react-router-dom"
+import { useEffect } from "react"
+
+import { orderLoader } from "./loader"
+import { orderExpand } from "../order-detail/constants"
+import { RouteFocusModal } from "../../../components/route-modal"
+import { OrderEditForm } from "./components/order-edit-form"
+
+/**
+ * Flag to ensure OE creation in useEffect is only executed once
+ */
+let isOECreationRunning = false
+
+export const OrderEdit = () => {
+ const { mutateAsync: createOrderEdit } = useAdminCreateOrderEdit()
+ const initialData = useLoaderData() as Awaited>
+
+ const { id } = useParams()
+
+ const { order, isLoading, isError, error } = useAdminOrder(
+ id!,
+ {
+ expand: orderExpand,
+ },
+ {
+ initialData,
+ }
+ )
+
+ // find created OE - there should exist only one per Order
+ const _orderEdit = order?.edits.find((oe) => oe.status === "created")
+
+ const { order_edit: orderEdit } = useAdminOrderEdit(
+ _orderEdit?.id as unknown as string,
+ {
+ expand: "changes,items,items.variant",
+ },
+ { enabled: !!_orderEdit?.id }
+ )
+
+ useEffect(() => {
+ ;(async () => {
+ if (!order || !!_orderEdit || isOECreationRunning) {
+ return
+ }
+
+ isOECreationRunning = true
+ await createOrderEdit({
+ order_id: order.id,
+ // created_by: // TODO
+ })
+ isOECreationRunning = false
+ })()
+ }, [order])
+
+ if (isLoading || !order || !orderEdit) {
+ // TODO: Add loader
+ return null
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+ {!isLoading && order && (
+
+ )}
+
+ )
+}