fix(draft-order, medusa, types): ui polish (#13376)

**What**
- ensure unique promotion changes are displayed in the activity section
- check linked promotions by id and not code
- list promotions - add id param
- make address payload fields nullish
- Add items view UI tweaks

---

CLOSES SUP-2400
This commit is contained in:
Frane Polić authored and GitHub committed 2025-09-05 06:55:07 +00:00
1 parent e67974ffe5
commit 0000ed6998
10 files changed
+114 -77

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/draft-order": patch
"@medusajs/types": patch
"@medusajs/medusa": patch
---
fix(draft-order,medusa,types): draft order promotion UI polish
@@ -1078,6 +1078,11 @@ export interface BaseOrderChange {
* The order change action details. * The order change action details.
*/ */
export interface BaseOrderChangeAction { export interface BaseOrderChangeAction {
/**
* The ordering of the order change action
*/
ordering: number
/** /**
* The ID of the order change action * The ID of the order change action
*/ */
@@ -19,6 +19,10 @@ export interface AdminGetPromotionsParams
* Filter by promotion code. * Filter by promotion code.
*/ */
code?: string | string[] | OperatorMap<string> code?: string | string[] | OperatorMap<string>
/**
* Filter by promotion ID.
*/
id?: string[] | string | OperatorMap<string>
/** /**
* Filter by campaign ID to retrieve promotions by campaign. * Filter by campaign ID to retrieve promotions by campaign.
*/ */
@@ -51,7 +55,9 @@ export interface AdminGetPromotionsParams
/** /**
* Filter by the promotion's application method type. * Filter by the promotion's application method type.
*/ */
application_method_type?: ApplicationMethodTypeValues | ApplicationMethodTypeValues[] application_method_type?:
| ApplicationMethodTypeValues
| ApplicationMethodTypeValues[]
/** /**
* An array of filters to apply on the entity, where each item in the array is joined with an "and" condition. * An array of filters to apply on the entity, where each item in the array is joined with an "and" condition.
*/ */
+10 -10
View File
@@ -34,52 +34,52 @@ export interface UpsertOrderAddressDTO {
/** /**
* The company name. * The company name.
*/ */
company?: string company?: string | null
/** /**
* The first name of the customer. * The first name of the customer.
*/ */
first_name?: string first_name?: string | null
/** /**
* The last name of the customer. * The last name of the customer.
*/ */
last_name?: string last_name?: string | null
/** /**
* The address 1 of the address. * The address 1 of the address.
*/ */
address_1?: string address_1?: string | null
/** /**
* The address 2 of the address. * The address 2 of the address.
*/ */
address_2?: string address_2?: string | null
/** /**
* The city of the address. * The city of the address.
*/ */
city?: string city?: string | null
/** /**
* The country code of the address. * The country code of the address.
*/ */
country_code?: string country_code?: string | null
/** /**
* The lower-case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) province of the address. * The lower-case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) province of the address.
*/ */
province?: string province?: string | null
/** /**
* The postal code of the address. * The postal code of the address.
*/ */
postal_code?: string postal_code?: string | null
/** /**
* The phone of the address. * The phone of the address.
*/ */
phone?: string phone?: string | null
/** /**
* Holds custom data in key-value pairs. * Holds custom data in key-value pairs.
@@ -26,6 +26,9 @@ export const AdminGetPromotionsParamsFields = z.object({
code: z code: z
.union([z.string(), z.array(z.string()), createOperatorMap()]) .union([z.string(), z.array(z.string()), createOperatorMap()])
.optional(), .optional(),
id: z
.union([z.string(), z.array(z.string()), createOperatorMap()])
.optional(),
campaign_id: z.union([z.string(), z.array(z.string())]).optional(), campaign_id: z.union([z.string(), z.array(z.string())]).optional(),
application_method: z application_method: z
.object({ .object({
@@ -2,17 +2,17 @@ import { z } from "zod"
export const AddressPayload = z export const AddressPayload = z
.object({ .object({
first_name: z.string().optional(), first_name: z.string().nullish(),
last_name: z.string().optional(), last_name: z.string().nullish(),
phone: z.string().optional(), phone: z.string().nullish(),
company: z.string().optional(), company: z.string().nullish(),
address_1: z.string().optional(), address_1: z.string().nullish(),
address_2: z.string().optional(), address_2: z.string().nullish(),
city: z.string().optional(), city: z.string().nullish(),
country_code: z.string().optional(), country_code: z.string().nullish(),
province: z.string().optional(), province: z.string().nullish(),
postal_code: z.string().optional(), postal_code: z.string().nullish(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.unknown()).nullish(),
}) })
.strict() .strict()
@@ -26,7 +26,7 @@ export const ActivitySection = ({ order, changes }: ActivitySectionProps) => {
) )
return ( return (
<Container className="p-0 overflow-hidden"> <Container className="overflow-hidden p-0">
<div className="px-6 py-4"> <div className="px-6 py-4">
<Heading>Activity</Heading> <Heading>Activity</Heading>
</div> </div>
@@ -83,9 +83,9 @@ const CollapsibleActivityItemList = ({
{!open && ( {!open && (
<div className="grid grid-cols-[20px_1fr] items-start gap-2"> <div className="grid grid-cols-[20px_1fr] items-start gap-2">
<div className="flex size-full flex-col items-center"> <div className="flex size-full flex-col items-center">
<div className="border-ui-border-strong w-px flex-1 bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-right bg-repeat-y bg-clip-content" /> <div className="border-ui-border-strong w-px flex-1 bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-clip-content bg-right bg-repeat-y" />
</div> </div>
<Collapsible.Trigger className="text-left p-0 m-0 pb-4 text-ui-fg-muted hover:text-ui-fg-base focus:text-ui-fg-base outline-none transition-colors"> <Collapsible.Trigger className="text-ui-fg-muted hover:text-ui-fg-base focus:text-ui-fg-base m-0 p-0 pb-4 text-left outline-none transition-colors">
<Text size="small" leading="compact" weight="plus"> <Text size="small" leading="compact" weight="plus">
{`Show ${items.length} more ${ {`Show ${items.length} more ${
items.length === 1 ? "activity" : "activities" items.length === 1 ? "activity" : "activities"
@@ -130,22 +130,22 @@ const ActivityItem = ({ item, isFirst = false }: ActivityItemProps) => {
return ( return (
<div <div
className={clx("grid grid-cols-[20px_1fr] items-start gap-x-2 w-full")} className={clx("grid w-full grid-cols-[20px_1fr] items-start gap-x-2")}
> >
<div className="flex flex-col items-center gap-0.5 h-full"> <div className="flex h-full flex-col items-center gap-0.5">
<div className="size-5 flex items-center justify-center"> <div className="flex size-5 items-center justify-center">
<div className="size-2.5 rounded-full shadow-borders-base flex items-center justify-center"> <div className="shadow-borders-base flex size-2.5 items-center justify-center rounded-full">
<div className="size-1.5 rounded-full bg-ui-tag-neutral-icon" /> <div className="bg-ui-tag-neutral-icon size-1.5 rounded-full" />
</div> </div>
</div> </div>
{!isFirst && ( {!isFirst && (
<div className="flex flex-1 items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="h-full w-px bg-ui-border-base" /> <div className="bg-ui-border-base h-full w-px" />
</div> </div>
)} )}
</div> </div>
<div className={clx("flex flex-col", !isFirst && "pb-4")}> <div className={clx("flex flex-col", !isFirst && "pb-4")}>
<div className="flex items-center gap-x-2 justify-between"> <div className="flex items-center justify-between gap-x-2">
<Text size="small" weight="plus" leading="compact"> <Text size="small" weight="plus" leading="compact">
{item.label} {item.label}
</Text> </Text>
@@ -159,10 +159,10 @@ const ActivityItem = ({ item, isFirst = false }: ActivityItemProps) => {
</div> </div>
{item.content && renderContent(item.content)} {item.content && renderContent(item.content)}
{item.userId && ( {item.userId && (
<div className="pt-2 text-ui-fg-muted"> <div className="text-ui-fg-muted pt-2">
{isUserLoaded ? ( {isUserLoaded ? (
<Link to={`/settings/users/${user.id}`} className="w-fit"> <Link to={`/settings/users/${user.id}`} className="w-fit">
<div className="flex items-center gap-x-1.5 w-fit"> <div className="flex w-fit items-center gap-x-1.5">
<Text size="small">By</Text> <Text size="small">By</Text>
<Avatar <Avatar
size="2xsmall" size="2xsmall"
@@ -179,8 +179,8 @@ const ActivityItem = ({ item, isFirst = false }: ActivityItemProps) => {
) : ( ) : (
<div className="flex items-center gap-x-1.5"> <div className="flex items-center gap-x-1.5">
<Text size="small">By</Text> <Text size="small">By</Text>
<Skeleton className="rounded-full w-5 h-5" /> <Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="w-[75px] h-4" /> <Skeleton className="h-4 w-[75px]" />
</div> </div>
)} )}
</div> </div>
@@ -215,7 +215,14 @@ function getEditActivityItems(
promotionsRemoved: 0, promotionsRemoved: 0,
} }
for (const action of change.actions) { const orderedActions = change.actions.sort((a, b) => {
return a.ordering - b.ordering
})
const addedPromotionMap = new Map<string, true>()
const removedPromotionMap = new Map<string, true>()
for (const action of orderedActions) {
if (!action.details) { if (!action.details) {
continue continue
} }
@@ -234,13 +241,22 @@ function getEditActivityItems(
case "SHIPPING_REMOVE": case "SHIPPING_REMOVE":
counts.shippingMethodsRemoved += 1 counts.shippingMethodsRemoved += 1
break break
case "PROMOTION_ADD": case "PROMOTION_ADD": {
counts.promotionsAdded += 1 addedPromotionMap.set(action.reference_id!, true)
break break
case "PROMOTION_REMOVE": }
counts.promotionsRemoved += 1 case "PROMOTION_REMOVE": {
if (addedPromotionMap.has(action.reference_id!)) {
addedPromotionMap.delete(action.reference_id!)
} else {
removedPromotionMap.set(action.reference_id!, true)
}
break break
}
} }
counts.promotionsAdded = addedPromotionMap.size
counts.promotionsRemoved = removedPromotionMap.size
} }
const createActivityItem = ( const createActivityItem = (
@@ -5,10 +5,10 @@ export const addressSchema = z.object({
first_name: z.string().min(1), first_name: z.string().min(1),
last_name: z.string().min(1), last_name: z.string().min(1),
address_1: z.string().min(1), address_1: z.string().min(1),
address_2: z.string().optional(), address_2: z.string().nullish(),
company: z.string().optional(), company: z.string().nullish(),
city: z.string().min(1), city: z.string().min(1),
province: z.string().optional(), province: z.string().nullish(),
postal_code: z.string().min(1), postal_code: z.string().min(1),
phone: z.string().optional(), phone: z.string().nullish(),
}) })
@@ -216,7 +216,7 @@ const ItemsForm = ({ preview, currencyCode }: ItemsFormProps) => {
</RouteFocusModal.Title> </RouteFocusModal.Title>
<RouteFocusModal.Description asChild> <RouteFocusModal.Description asChild>
<Text size="small" className="text-ui-fg-subtle"> <Text size="small" className="text-ui-fg-subtle">
Edit the items in the draft order. Edit the items in the draft order
</Text> </Text>
</RouteFocusModal.Description> </RouteFocusModal.Description>
</div> </div>
@@ -261,7 +261,7 @@ const ItemsForm = ({ preview, currencyCode }: ItemsFormProps) => {
</div> </div>
<div className="bg-ui-bg-subtle shadow-elevation-card-rest rounded-xl"> <div className="bg-ui-bg-subtle shadow-elevation-card-rest rounded-xl">
<div className="px-[5px]"> <div className="px-[5px]">
<div className="grid grid-cols-[1fr_1fr_1fr_28px] gap-3 px-4 py-2 text-ui-fg-muted"> <div className="text-ui-fg-muted grid grid-cols-[2fr_1fr_2fr_28px] gap-3 px-4 py-2">
<div> <div>
<Text size="small" weight="plus"> <Text size="small" weight="plus">
Item Item
@@ -282,7 +282,7 @@ const ItemsForm = ({ preview, currencyCode }: ItemsFormProps) => {
</div> </div>
<div className="flex flex-col gap-y-1.5 px-[5px] pb-[5px]"> <div className="flex flex-col gap-y-1.5 px-[5px] pb-[5px]">
{itemCount <= 0 ? ( {itemCount <= 0 ? (
<div className="flex items-center justify-center gap-x-3 bg-ui-bg-base rounded-lg p-4 shadow-elevation-card-rest flex-col gap-1"> <div className="bg-ui-bg-base shadow-elevation-card-rest flex flex-col items-center justify-center gap-1 gap-x-3 rounded-lg p-4">
<Text size="small" weight="plus" leading="compact"> <Text size="small" weight="plus" leading="compact">
There are no items in this order There are no items in this order
</Text> </Text>
@@ -300,7 +300,7 @@ const ItemsForm = ({ preview, currencyCode }: ItemsFormProps) => {
/> />
)) ))
) : ( ) : (
<div className="flex items-center justify-center gap-x-3 bg-ui-bg-base rounded-lg p-4 shadow-elevation-card-rest flex-col gap-1"> <div className="bg-ui-bg-base shadow-elevation-card-rest flex flex-col items-center justify-center gap-1 gap-x-3 rounded-lg p-4">
<Text size="small" weight="plus" leading="compact"> <Text size="small" weight="plus" leading="compact">
No items found No items found
</Text> </Text>
@@ -348,7 +348,7 @@ const ItemsForm = ({ preview, currencyCode }: ItemsFormProps) => {
</StackedFocusModal> </StackedFocusModal>
</RouteFocusModal.Body> </RouteFocusModal.Body>
<RouteFocusModal.Footer> <RouteFocusModal.Footer>
<div className="flex items-center gap-x-2 justify-end"> <div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary" type="button"> <Button size="small" variant="secondary" type="button">
Cancel Cancel
@@ -461,8 +461,8 @@ const VariantItem = ({ item, preview, currencyCode }: ItemProps) => {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_28px] gap-3 px-4 py-2 bg-ui-bg-base shadow-elevation-card-rest rounded-lg items-center"> <div className="bg-ui-bg-base shadow-elevation-card-rest grid grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,2fr)_28px] items-center gap-3 rounded-lg px-4 py-2">
<div className="flex items-center gap-x-3 w-full"> <div className="flex w-full items-center gap-x-3">
<Thumbnail <Thumbnail
thumbnail={item.thumbnail} thumbnail={item.thumbnail}
alt={item.product_title ?? undefined} alt={item.product_title ?? undefined}
@@ -490,7 +490,7 @@ const VariantItem = ({ item, preview, currencyCode }: ItemProps) => {
</div> </div>
</div> </div>
{editing ? ( {editing ? (
<div className="flex-1 w-full"> <div className="w-full flex-1">
<Form.Field <Form.Field
control={form.control} control={form.control}
name="quantity" name="quantity"
@@ -506,14 +506,14 @@ const VariantItem = ({ item, preview, currencyCode }: ItemProps) => {
/> />
</div> </div>
) : ( ) : (
<div className="flex-1 w-full"> <div className="w-full flex-1">
<Text size="small" weight="plus"> <Text size="small" weight="plus">
{item.quantity} {item.quantity}
</Text> </Text>
</div> </div>
)} )}
{editing ? ( {editing ? (
<div className="flex-1 w-full"> <div className="w-full flex-1">
<Form.Field <Form.Field
control={form.control} control={form.control}
name="unit_price" name="unit_price"
@@ -536,7 +536,7 @@ const VariantItem = ({ item, preview, currencyCode }: ItemProps) => {
/> />
</div> </div>
) : ( ) : (
<div className="flex-1 flex items-center justify-end w-full"> <div className="flex w-full flex-1 items-center justify-end">
<Text size="small" weight="plus"> <Text size="small" weight="plus">
{getLocaleAmount(item.unit_price, currencyCode)} {getLocaleAmount(item.unit_price, currencyCode)}
</Text> </Text>
@@ -670,7 +670,7 @@ const CustomItem = ({ item, preview, currencyCode }: ItemProps) => {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_28px] gap-3 px-4 py-2 bg-ui-bg-base shadow-elevation-card-rest rounded-lg items-center"> <div className="bg-ui-bg-base shadow-elevation-card-rest grid grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,2fr)_28px] items-center gap-3 rounded-lg px-4 py-2">
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<Thumbnail <Thumbnail
thumbnail={item.thumbnail} thumbnail={item.thumbnail}
@@ -737,7 +737,7 @@ const CustomItem = ({ item, preview, currencyCode }: ItemProps) => {
}} }}
/> />
) : ( ) : (
<div className="flex-1 flex items-center justify-end"> <div className="flex flex-1 items-center justify-end">
<Text size="small" weight="plus"> <Text size="small" weight="plus">
{getLocaleAmount(item.unit_price, currencyCode)} {getLocaleAmount(item.unit_price, currencyCode)}
</Text> </Text>
@@ -917,7 +917,7 @@ const ExistingItemsForm = ({ orderId, items }: ExistingItemsFormProps) => {
/> />
</StackedFocusModal.Body> </StackedFocusModal.Body>
<StackedFocusModal.Footer> <StackedFocusModal.Footer>
<div className="flex items-center gap-x-2 justify-end"> <div className="flex items-center justify-end gap-x-2">
<StackedFocusModal.Close asChild> <StackedFocusModal.Close asChild>
<Button size="small" variant="secondary" type="button"> <Button size="small" variant="secondary" type="button">
Cancel Cancel
@@ -1120,9 +1120,9 @@ const CustomItemForm = ({ orderId, currencyCode }: CustomItemFormProps) => {
<Form.Label>Quantity</Form.Label> <Form.Label>Quantity</Form.Label>
<Form.Hint>Enter the quantity of the item</Form.Hint> <Form.Hint>Enter the quantity of the item</Form.Hint>
</div> </div>
<div className="flex-1 w-full"> <div className="w-full flex-1">
<Form.Control> <Form.Control>
<div className="flex-1 w-full"> <div className="w-full flex-1">
<NumberInput {...field} className="w-full" /> <NumberInput {...field} className="w-full" />
</div> </div>
</Form.Control> </Form.Control>
@@ -1136,7 +1136,7 @@ const CustomItemForm = ({ orderId, currencyCode }: CustomItemFormProps) => {
</div> </div>
</StackedFocusModal.Body> </StackedFocusModal.Body>
<StackedFocusModal.Footer> <StackedFocusModal.Footer>
<div className="flex items-center gap-x-2 justify-end"> <div className="flex items-center justify-end gap-x-2">
<StackedFocusModal.Close asChild> <StackedFocusModal.Close asChild>
<Button size="small" variant="secondary" type="button"> <Button size="small" variant="secondary" type="button">
Cancel Cancel
@@ -78,24 +78,24 @@ const PromotionForm = ({ preview }: PromotionFormProps) => {
const { mutateAsync: addPromotions, isPending: isAddingPromotions } = const { mutateAsync: addPromotions, isPending: isAddingPromotions } =
useDraftOrderAddPromotions(preview.id) useDraftOrderAddPromotions(preview.id)
const promoCodes = getPromotionCodes(items, shipping_methods) const promoIds = getPromotionIds(items, shipping_methods)
const { promotions, isPending, isError, error } = usePromotions( const { promotions, isPending, isError, error } = usePromotions(
{ {
code: promoCodes, id: promoIds,
}, },
{ {
enabled: !!promoCodes.length, enabled: !!promoIds.length,
} }
) )
const comboboxData = useComboboxData({ const comboboxData = useComboboxData({
queryKey: ["promotions", "combobox", promoCodes], queryKey: ["promotions", "combobox", promoIds],
queryFn: async (params) => { queryFn: async (params) => {
return await sdk.admin.promotion.list({ return await sdk.admin.promotion.list({
...params, ...params,
code: { id: {
$nin: promoCodes, $nin: promoIds,
}, },
}) })
}, },
@@ -261,7 +261,7 @@ const PromotionItem = ({
<div <div
key={promotion.id} key={promotion.id}
className={clx( className={clx(
"px-3 py-2 rounded-lg bg-ui-bg-component shadow-elevation-card-rest flex items-center justify-between", "bg-ui-bg-component shadow-elevation-card-rest flex items-center justify-between rounded-lg px-3 py-2",
{ {
"animate-pulse": isLoading, "animate-pulse": isLoading,
} }
@@ -271,7 +271,7 @@ const PromotionItem = ({
<Text size="small" weight="plus" leading="compact"> <Text size="small" weight="plus" leading="compact">
{promotion.code} {promotion.code}
</Text> </Text>
<div className="flex items-center gap-1.5 text-ui-fg-subtle"> <div className="text-ui-fg-subtle flex items-center gap-1.5">
{displayValue && ( {displayValue && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Text size="small" leading="compact"> <Text size="small" leading="compact">
@@ -337,17 +337,17 @@ const formatPercentage = (value?: number | null, isPercentageValue = false) => {
return formatter.format(val) return formatter.format(val)
} }
function getPromotionCodes( function getPromotionIds(
items: HttpTypes.AdminOrderPreview["items"], items: HttpTypes.AdminOrderPreview["items"],
shippingMethods: HttpTypes.AdminOrderPreview["shipping_methods"] shippingMethods: HttpTypes.AdminOrderPreview["shipping_methods"]
) { ) {
const codes = new Set<string>() const promotionIds = new Set<string>()
for (const item of items) { for (const item of items) {
if (item.adjustments) { if (item.adjustments) {
for (const adjustment of item.adjustments) { for (const adjustment of item.adjustments) {
if (adjustment.code) { if (adjustment.promotion_id) {
codes.add(adjustment.code) promotionIds.add(adjustment.promotion_id)
} }
} }
} }
@@ -356,14 +356,14 @@ function getPromotionCodes(
for (const shippingMethod of shippingMethods) { for (const shippingMethod of shippingMethods) {
if (shippingMethod.adjustments) { if (shippingMethod.adjustments) {
for (const adjustment of shippingMethod.adjustments) { for (const adjustment of shippingMethod.adjustments) {
if (adjustment.code) { if (adjustment.promotion_id) {
codes.add(adjustment.code) promotionIds.add(adjustment.promotion_id)
} }
} }
} }
} }
return Array.from(codes) return Array.from(promotionIds)
} }
export default Promotions export default Promotions