@vendure/dashboard 3.3.6-master-202507090236 → 3.3.6-master-202507110238

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/plugin/tests/barrel-exports.spec.js +1 -1
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
  4. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
  5. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
  6. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
  7. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
  8. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
  9. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
  10. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +268 -31
  11. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
  12. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
  13. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
  14. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
  15. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
  16. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
  17. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -17
  18. package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
  19. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
  20. package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
  21. package/src/lib/components/data-display/date-time.tsx +7 -1
  22. package/src/lib/components/data-input/relation-input.tsx +11 -0
  23. package/src/lib/components/data-input/relation-selector.tsx +9 -2
  24. package/src/lib/components/data-table/data-table-utils.ts +34 -0
  25. package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
  26. package/src/lib/components/data-table/data-table.tsx +5 -2
  27. package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
  28. package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
  29. package/src/lib/components/shared/product-variant-selector.tsx +28 -4
  30. package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
  31. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
  32. package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
  33. package/src/lib/framework/extension-api/types/layout.ts +21 -6
  34. package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
  35. package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
  36. package/src/lib/framework/page/use-detail-page.ts +10 -7
  37. package/vite/tests/barrel-exports.spec.ts +1 -1
@@ -0,0 +1,364 @@
1
+ import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
2
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
3
+ import { Alert, AlertDescription, AlertTitle } from '@/vdb/components/ui/alert.js';
4
+ import { Button } from '@/vdb/components/ui/button.js';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/vdb/components/ui/card.js';
6
+ import {
7
+ Dialog,
8
+ DialogClose,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from '@/vdb/components/ui/dialog.js';
15
+ import { FormControl, FormField } from '@/vdb/components/ui/form.js';
16
+ import { Textarea } from '@/vdb/components/ui/textarea.js';
17
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
18
+ import { api } from '@/vdb/graphql/api.js';
19
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
20
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
21
+ import { useMutation } from '@tanstack/react-query';
22
+ import { ResultOf, VariablesOf } from 'gql.tada';
23
+ import { CheckIcon } from 'lucide-react';
24
+ import { useEffect, useRef } from 'react';
25
+ import { FormProvider, useForm } from 'react-hook-form';
26
+ import { modifyOrderDocument, orderDetailDocument } from '../orders.graphql.js';
27
+ import { OrderTable } from './order-table.js';
28
+
29
+ export type OrderFragment = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
30
+ export type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
31
+
32
+ interface OrderModificationPreviewDialogProps {
33
+ open: boolean;
34
+ onOpenChange: (open: boolean) => void;
35
+ orderSnapshot: OrderFragment;
36
+ modifyOrderInput: ModifyOrderInput;
37
+ /**
38
+ * The price difference between the order snapshot and the preview order.
39
+ * If the dialog is cancelled, this will be undefined.
40
+ */
41
+ onResolve: (priceDifference?: number) => void;
42
+ }
43
+
44
+ interface PaymentRefundForm {
45
+ payments: Record<string, number>;
46
+ note: string;
47
+ }
48
+
49
+ export function OrderModificationPreviewDialog({
50
+ open,
51
+ onOpenChange,
52
+ orderSnapshot,
53
+ modifyOrderInput,
54
+ onResolve,
55
+ }: Readonly<OrderModificationPreviewDialogProps>) {
56
+ const { i18n } = useLingui();
57
+ const { formatCurrency } = useLocalFormat();
58
+ // Use a ref to track the last input sent to avoid duplicate calls
59
+ const lastInputRef = useRef<ModifyOrderInput | null>(null);
60
+ const previewMutation = useMutation({
61
+ mutationFn: api.mutate(addCustomFields(modifyOrderDocument)),
62
+ });
63
+
64
+ // Create form with dynamic fields for each payment
65
+ const refundForm = useForm<PaymentRefundForm>({
66
+ defaultValues: {
67
+ note: '',
68
+ payments:
69
+ orderSnapshot.payments?.reduce(
70
+ (acc, payment) => {
71
+ acc[payment.id] = 0;
72
+ return acc;
73
+ },
74
+ {} as Record<string, number>,
75
+ ) || {},
76
+ },
77
+ });
78
+
79
+ const confirmMutation = useMutation({
80
+ mutationFn: (input: ModifyOrderInput) => api.mutate(addCustomFields(modifyOrderDocument))({ input }),
81
+ });
82
+
83
+ // Trigger preview when dialog opens or input changes (while open)
84
+ useEffect(() => {
85
+ if (open) {
86
+ // Only trigger if input actually changed
87
+ if (
88
+ !lastInputRef.current ||
89
+ JSON.stringify(lastInputRef.current) !== JSON.stringify(modifyOrderInput)
90
+ ) {
91
+ const input = { ...modifyOrderInput, dryRun: true };
92
+ previewMutation.mutate({ input });
93
+ lastInputRef.current = modifyOrderInput;
94
+ }
95
+ }
96
+ }, [open, modifyOrderInput]);
97
+
98
+ const previewOrder =
99
+ previewMutation.data?.modifyOrder?.__typename === 'Order' ? previewMutation.data.modifyOrder : null;
100
+ const error =
101
+ previewMutation.data && previewMutation.data.modifyOrder?.__typename !== 'Order'
102
+ ? previewMutation.data.modifyOrder?.message || i18n.t('Unknown error')
103
+ : previewMutation.error?.message || null;
104
+ const loading = previewMutation.isPending;
105
+
106
+ // Price difference
107
+ const priceDifference = previewOrder ? previewOrder.totalWithTax - orderSnapshot.totalWithTax : 0;
108
+ const formattedDiff = previewOrder
109
+ ? formatCurrency(Math.abs(priceDifference), previewOrder.currencyCode)
110
+ : '';
111
+
112
+ // Calculate total refund amount from form
113
+ const totalRefundAmount =
114
+ orderSnapshot.payments?.reduce((total, payment) => {
115
+ const refundAmount = refundForm.watch(`payments.${payment.id}`) || 0;
116
+ return total + refundAmount;
117
+ }, 0) || 0;
118
+
119
+ // Check if total refund matches the required amount
120
+ const isRefundComplete = priceDifference > 0 || totalRefundAmount >= Math.abs(priceDifference);
121
+ const remainingAmount = Math.abs(priceDifference) - totalRefundAmount;
122
+
123
+ // Confirm handler
124
+ const handleConfirm = async () => {
125
+ if (!previewOrder) return;
126
+ const input: ModifyOrderInput = { ...modifyOrderInput, dryRun: false };
127
+ if (priceDifference < 0) {
128
+ // Create refunds array from form data
129
+ const { note, payments } = refundForm.getValues();
130
+ const refunds = Object.entries(payments)
131
+ .filter(([_, amount]) => amount > 0)
132
+ .map(([paymentId, amount]) => ({
133
+ paymentId,
134
+ amount,
135
+ reason: note,
136
+ }));
137
+
138
+ input.refunds = refunds;
139
+ }
140
+ await confirmMutation.mutateAsync(input);
141
+ onResolve(priceDifference);
142
+ };
143
+
144
+ return (
145
+ <Dialog open={open} onOpenChange={onOpenChange}>
146
+ <DialogContent className="min-w-[80vw] max-h-[90vh] p-8 overflow-hidden flex flex-col">
147
+ <DialogHeader>
148
+ <DialogTitle>
149
+ <Trans>Preview order modifications</Trans>
150
+ </DialogTitle>
151
+ <DialogDescription>
152
+ <Trans>Review the changes before applying them to the order.</Trans>
153
+ </DialogDescription>
154
+ </DialogHeader>
155
+ <div className="space-y-4 flex-1 overflow-y-auto">
156
+ {loading && (
157
+ <div className="text-center py-8">
158
+ <Trans>Loading preview…</Trans>
159
+ </div>
160
+ )}
161
+ {error && <div className="text-destructive py-2">{error}</div>}
162
+ {previewOrder && !loading && !error && (
163
+ <>
164
+ <OrderTable order={previewOrder} />
165
+ {/* Refund/payment UI using Alert */}
166
+ {priceDifference < 0 && (
167
+ <>
168
+ <Alert variant="destructive">
169
+ <AlertTitle>
170
+ <Trans>Refund required</Trans>
171
+ </AlertTitle>
172
+ <AlertDescription>
173
+ <Trans>
174
+ A refund of {formattedDiff} is required. Select payment
175
+ amounts and enter a note to proceed.
176
+ </Trans>
177
+ </AlertDescription>
178
+ </Alert>
179
+ <FormProvider {...refundForm}>
180
+ <form className="space-y-4 mt-4">
181
+ {/* Payment Cards */}
182
+ <div className="space-y-3">
183
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
184
+ {orderSnapshot.payments?.map(payment => (
185
+ <Card key={payment.id} className="">
186
+ <CardHeader className="">
187
+ <CardTitle className="flex gap-2 items-baseline">
188
+ <span className="">{payment.method}</span>
189
+ <span className="text-xs text-muted-foreground">
190
+ ID: {payment.transactionId}
191
+ </span>
192
+ </CardTitle>
193
+ <Button
194
+ type="button"
195
+ size="sm"
196
+ variant="outline"
197
+ className=""
198
+ onClick={() => {
199
+ const currentRefundAmount =
200
+ refundForm.getValues(
201
+ `payments.${payment.id}`,
202
+ ) || 0;
203
+ const availableAmount =
204
+ payment.amount;
205
+ const remainingNeeded =
206
+ Math.abs(priceDifference) -
207
+ totalRefundAmount;
208
+
209
+ // Calculate how much we can still refund from this payment method
210
+ const remainingFromThisPayment =
211
+ availableAmount -
212
+ currentRefundAmount;
213
+
214
+ // Take the minimum of what's needed and what's available
215
+ const amountToAdd = Math.min(
216
+ remainingFromThisPayment,
217
+ remainingNeeded,
218
+ );
219
+
220
+ if (amountToAdd > 0) {
221
+ refundForm.setValue(
222
+ `payments.${payment.id}`,
223
+ currentRefundAmount +
224
+ amountToAdd,
225
+ );
226
+ }
227
+ }}
228
+ >
229
+ <Trans>Refund from this method</Trans>
230
+ </Button>
231
+ </CardHeader>
232
+ <CardContent className="pt-0">
233
+ <div className="flex flex-col gap-3">
234
+ <div className="text-sm text-muted-foreground">
235
+ <Trans>Available:</Trans>{' '}
236
+ {formatCurrency(
237
+ payment.amount,
238
+ orderSnapshot.currencyCode,
239
+ )}
240
+ </div>
241
+ <div className="w-full">
242
+ <FormField
243
+ name={`payments.${payment.id}`}
244
+ control={refundForm.control}
245
+ render={({ field }) => (
246
+ <FormControl>
247
+ <MoneyInput
248
+ value={
249
+ field.value || 0
250
+ }
251
+ onChange={
252
+ field.onChange
253
+ }
254
+ currency={
255
+ orderSnapshot.currencyCode
256
+ }
257
+ />
258
+ </FormControl>
259
+ )}
260
+ />
261
+ </div>
262
+ </div>
263
+ </CardContent>
264
+ </Card>
265
+ ))}
266
+ </div>
267
+ </div>
268
+
269
+ {/* Total Refund Summary */}
270
+ <div className="bg-muted/50 rounded-lg pb-3">
271
+ <div className="flex items-center justify-between p-3">
272
+ <div className="flex items-center gap-2">
273
+ <span className="text-sm font-medium">
274
+ <Trans>Total refund:</Trans>
275
+ </span>
276
+ <span className="text-sm">
277
+ {formatCurrency(
278
+ totalRefundAmount,
279
+ orderSnapshot.currencyCode,
280
+ )}
281
+ </span>
282
+ {isRefundComplete && (
283
+ <CheckIcon className="h-4 w-4 text-green-600" />
284
+ )}
285
+ </div>
286
+ {!isRefundComplete && (
287
+ <div className="text-sm text-muted-foreground">
288
+ <Trans>Remaining:</Trans>{' '}
289
+ {formatCurrency(
290
+ remainingAmount,
291
+ orderSnapshot.currencyCode,
292
+ )}
293
+ </div>
294
+ )}
295
+ </div>
296
+ <div className="px-3">
297
+ <FormFieldWrapper
298
+ name="note"
299
+ control={refundForm.control}
300
+ rules={{ required: true }}
301
+ render={({ field }) => (
302
+ <Textarea
303
+ {...field}
304
+ className="bg-background"
305
+ placeholder={i18n.t('Enter refund note')}
306
+ />
307
+ )}
308
+ />
309
+ </div>
310
+ </div>
311
+ </form>
312
+ </FormProvider>
313
+ </>
314
+ )}
315
+ <div className="w-full flex justify-end">
316
+ <div className="max-w-l mb-2">
317
+ {priceDifference > 0 && (
318
+ <Alert variant="destructive">
319
+ <AlertTitle>
320
+ <Trans>Additional payment required</Trans>
321
+ </AlertTitle>
322
+ <AlertDescription>
323
+ <Trans>
324
+ An additional payment of {formattedDiff} will be required.
325
+ </Trans>
326
+ </AlertDescription>
327
+ </Alert>
328
+ )}
329
+ {priceDifference === 0 && (
330
+ <Alert variant="default">
331
+ <AlertTitle>
332
+ <Trans>No payment or refund required</Trans>
333
+ </AlertTitle>
334
+ <AlertDescription>
335
+ <Trans>
336
+ No payment or refund is required for these changes.
337
+ </Trans>
338
+ </AlertDescription>
339
+ </Alert>
340
+ )}
341
+ </div>
342
+ </div>
343
+ </>
344
+ )}
345
+ </div>
346
+ <DialogFooter>
347
+ <DialogClose asChild>
348
+ <Button type="button" variant="secondary" onClick={() => onResolve()}>
349
+ <Trans>Cancel</Trans>
350
+ </Button>
351
+ </DialogClose>
352
+ <Button
353
+ type="button"
354
+ variant="default"
355
+ onClick={handleConfirm}
356
+ disabled={loading || !!error || confirmMutation.isPending || !isRefundComplete}
357
+ >
358
+ <Trans>Confirm</Trans>
359
+ </Button>
360
+ </DialogFooter>
361
+ </DialogContent>
362
+ </Dialog>
363
+ );
364
+ }
@@ -0,0 +1,222 @@
1
+ import { Trans } from '@/vdb/lib/trans.js';
2
+ import { ResultOf, VariablesOf } from 'gql.tada';
3
+ import { modifyOrderDocument, orderDetailDocument } from '../orders.graphql.js';
4
+
5
+ type OrderFragment = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
6
+ type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
7
+
8
+ type OrderLine = OrderFragment['lines'][number];
9
+
10
+ interface OrderModificationSummaryProps {
11
+ originalOrder: OrderFragment;
12
+ modifyOrderInput: ModifyOrderInput;
13
+ addedVariants: Map<string, any>; // For displaying added line info
14
+ eligibleShippingMethods: Array<{
15
+ id: string;
16
+ name: string;
17
+ }>;
18
+ }
19
+
20
+ interface LineAdjustment {
21
+ orderLineId: string;
22
+ name: string;
23
+ oldQuantity: number;
24
+ newQuantity: number;
25
+ oldCustomFields: Record<string, any>;
26
+ newCustomFields: Record<string, any>;
27
+ }
28
+
29
+ export function OrderModificationSummary({
30
+ originalOrder,
31
+ modifyOrderInput,
32
+ addedVariants,
33
+ eligibleShippingMethods,
34
+ }: Readonly<OrderModificationSummaryProps>) {
35
+ // Map by line id for quick lookup
36
+ const originalLineMap = new Map(originalOrder.lines.map(line => [line.id, line]));
37
+
38
+ // Adjusted lines: in both, but quantity changed
39
+ const adjustedLines = (modifyOrderInput.adjustOrderLines ?? [])
40
+ .map((adj: NonNullable<ModifyOrderInput['adjustOrderLines']>[number] & { customFields?: any }) => {
41
+ const orig = originalLineMap.get(adj.orderLineId);
42
+ if (
43
+ orig &&
44
+ (adj.quantity !== orig.quantity ||
45
+ JSON.stringify(adj.customFields) !== JSON.stringify((orig as any).customFields))
46
+ ) {
47
+ return {
48
+ orderLineId: adj.orderLineId,
49
+ name: orig.productVariant.name,
50
+ oldQuantity: orig.quantity,
51
+ newQuantity: adj.quantity,
52
+ oldCustomFields: (orig as any).customFields,
53
+ newCustomFields: adj.customFields,
54
+ };
55
+ }
56
+ return null;
57
+ })
58
+ .filter(Boolean) as Array<LineAdjustment>;
59
+
60
+ // Added lines: from addItems
61
+ const addedLines = (modifyOrderInput.addItems ?? [])
62
+ .map(item => {
63
+ const variantInfo = addedVariants.get(item.productVariantId);
64
+ return variantInfo
65
+ ? {
66
+ id: `added-${item.productVariantId}`,
67
+ name: variantInfo.productVariantName,
68
+ quantity: item.quantity,
69
+ }
70
+ : null;
71
+ })
72
+ .filter(Boolean) as Array<{ id: string; name: string; quantity: number }>;
73
+
74
+ // Removed lines: quantity set to 0 in adjustOrderLines
75
+ const removedLines = (modifyOrderInput.adjustOrderLines ?? [])
76
+ .map(adj => {
77
+ const orig = originalLineMap.get(adj.orderLineId);
78
+ if (orig && adj.quantity === 0) {
79
+ return orig;
80
+ }
81
+ return null;
82
+ })
83
+ .filter(Boolean) as OrderLine[];
84
+
85
+ // Coupon code changes
86
+ const originalCoupons = new Set(originalOrder.couponCodes ?? []);
87
+ const modifiedCoupons = new Set(modifyOrderInput.couponCodes ?? []);
88
+ const addedCouponCodes = Array.from(modifiedCoupons).filter(code => !originalCoupons.has(code));
89
+ const removedCouponCodes = Array.from(originalCoupons).filter(code => !modifiedCoupons.has(code));
90
+
91
+ // Shipping method change detection
92
+ const originalShippingMethodId = originalOrder.shippingLines?.[0]?.shippingMethod?.id;
93
+ const modifiedShippingMethodId = modifyOrderInput.shippingMethodIds?.[0];
94
+ let shippingMethodChanged = false;
95
+ let newShippingMethodName = '';
96
+ if (modifiedShippingMethodId && modifiedShippingMethodId !== originalShippingMethodId) {
97
+ shippingMethodChanged = true;
98
+ newShippingMethodName =
99
+ eligibleShippingMethods.find(m => m.id === modifiedShippingMethodId)?.name ||
100
+ modifiedShippingMethodId;
101
+ }
102
+
103
+ return (
104
+ <div className="text-sm">
105
+ {/* Address changes */}
106
+ {modifyOrderInput.updateShippingAddress && (
107
+ <div className="mb-2">
108
+ <span className="font-medium">
109
+ <Trans>Shipping address changed</Trans>
110
+ </span>
111
+ </div>
112
+ )}
113
+ {modifyOrderInput.updateBillingAddress && (
114
+ <div className="mb-2">
115
+ <span className="font-medium">
116
+ <Trans>Billing address changed</Trans>
117
+ </span>
118
+ </div>
119
+ )}
120
+ {shippingMethodChanged && (
121
+ <div className="mb-2">
122
+ <span className="font-medium">
123
+ <Trans>Shipping method changed</Trans>: {newShippingMethodName}
124
+ </span>
125
+ </div>
126
+ )}
127
+ {adjustedLines.length > 0 && (
128
+ <div className="mb-2">
129
+ <div className="font-medium">
130
+ <Trans>Adjusting {adjustedLines.length} lines</Trans>
131
+ </div>
132
+ <ul className="list-disc ml-4">
133
+ {adjustedLines.map(line => (
134
+ <li key={line.orderLineId} className="">
135
+ <div className="flex items-center gap-2">
136
+ {line.name}:{' '}
137
+ {line.oldQuantity !== line.newQuantity && (
138
+ <div>
139
+ <span className="text-muted-foreground">
140
+ {line.oldQuantity} →{' '}
141
+ </span>
142
+
143
+ {line.newQuantity}
144
+ </div>
145
+ )}
146
+ </div>
147
+ {JSON.stringify(line.oldCustomFields) !==
148
+ JSON.stringify(line.newCustomFields) && (
149
+ <span className="text-muted-foreground">
150
+ <Trans>Custom fields changed</Trans>
151
+ </span>
152
+ )}
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ </div>
157
+ )}
158
+ {addedLines.length > 0 && (
159
+ <div className="mb-2">
160
+ <div className="font-medium">
161
+ <Trans>Adding {addedLines.length} item(s)</Trans>
162
+ </div>
163
+ <ul className="list-disc ml-4">
164
+ {addedLines.map(line => (
165
+ <li key={line.id}>
166
+ {line.name} x {line.quantity}
167
+ </li>
168
+ ))}
169
+ </ul>
170
+ </div>
171
+ )}
172
+ {removedLines.length > 0 && (
173
+ <div className="mb-2">
174
+ <div className="font-medium">
175
+ <Trans>Removing {removedLines.length} item(s)</Trans>
176
+ </div>
177
+ <ul className="list-disc ml-4">
178
+ {removedLines.map(line => (
179
+ <li key={line.id}>{line.productVariant.name}</li>
180
+ ))}
181
+ </ul>
182
+ </div>
183
+ )}
184
+ {addedCouponCodes.length > 0 && (
185
+ <div className="mb-2">
186
+ <div className="font-medium">
187
+ <Trans>Added coupon codes</Trans>
188
+ </div>
189
+ <ul className="list-disc ml-4">
190
+ {addedCouponCodes.map(code => (
191
+ <li key={code}>{code}</li>
192
+ ))}
193
+ </ul>
194
+ </div>
195
+ )}
196
+ {removedCouponCodes.length > 0 && (
197
+ <div className="mb-2">
198
+ <div className="font-medium">
199
+ <Trans>Removed coupon codes</Trans>
200
+ </div>
201
+ <ul className="list-disc ml-4">
202
+ {removedCouponCodes.map(code => (
203
+ <li key={code}>{code}</li>
204
+ ))}
205
+ </ul>
206
+ </div>
207
+ )}
208
+ {adjustedLines.length === 0 &&
209
+ addedLines.length === 0 &&
210
+ removedLines.length === 0 &&
211
+ addedCouponCodes.length === 0 &&
212
+ removedCouponCodes.length === 0 &&
213
+ !modifyOrderInput.updateShippingAddress &&
214
+ !modifyOrderInput.updateBillingAddress &&
215
+ !shippingMethodChanged && (
216
+ <div className="text-muted-foreground">
217
+ <Trans>No modifications made</Trans>
218
+ </div>
219
+ )}
220
+ </div>
221
+ );
222
+ }