@vendure/dashboard 3.4.3-master-202509190229 → 3.4.3-master-202509230228

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 (60) hide show
  1. package/dist/vite/vite-plugin-config.js +1 -0
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_administrators/administrators.tsx +1 -2
  4. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
  5. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
  6. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
  7. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
  8. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
  9. package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
  10. package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
  11. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
  12. package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
  13. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
  14. package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
  15. package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
  16. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +302 -0
  17. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -0
  18. package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +61 -0
  19. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +17 -10
  20. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +31 -0
  21. package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +50 -0
  22. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +17 -290
  23. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +7 -39
  24. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +4 -26
  25. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +129 -0
  26. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +8 -0
  27. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
  28. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
  29. package/src/app/routes/_authenticated/_products/products.tsx +1 -2
  30. package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
  31. package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +251 -0
  32. package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
  33. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -3
  34. package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
  35. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
  36. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
  37. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
  38. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
  39. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
  40. package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
  41. package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
  42. package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
  43. package/src/lib/components/layout/nav-main.tsx +50 -25
  44. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  45. package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
  46. package/src/lib/components/shared/detail-page-button.tsx +6 -4
  47. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
  48. package/src/lib/components/shared/vendure-image.tsx +9 -1
  49. package/src/lib/framework/defaults.ts +24 -0
  50. package/src/lib/framework/extension-api/types/navigation.ts +8 -0
  51. package/src/lib/framework/layout-engine/page-layout.tsx +96 -9
  52. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
  53. package/src/lib/framework/page/list-page.tsx +7 -0
  54. package/src/lib/hooks/use-custom-field-config.ts +19 -2
  55. package/src/lib/index.ts +7 -1
  56. package/src/lib/providers/channel-provider.tsx +22 -6
  57. package/src/lib/providers/server-config.tsx +1 -0
  58. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
  59. package/src/app/routes/_authenticated/_roles/components/permissions-grid.tsx +0 -120
  60. package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
@@ -13,6 +13,22 @@ export function OrderTableTotals({ order, columnCount }: Readonly<OrderTableTota
13
13
 
14
14
  return (
15
15
  <>
16
+ {order.surcharges?.length > 0
17
+ ? order.surcharges.map((surcharge, index) => (
18
+ <TableRow key={`${surcharge.description}-${index}`}>
19
+ <TableCell colSpan={columnCount - 1} className="h-12">
20
+ <Trans>Surcharge</Trans>: {surcharge.description}
21
+ </TableCell>
22
+ <TableCell colSpan={1} className="h-12">
23
+ <MoneyGrossNet
24
+ priceWithTax={surcharge.priceWithTax}
25
+ price={surcharge.price}
26
+ currencyCode={currencyCode}
27
+ />
28
+ </TableCell>
29
+ </TableRow>
30
+ ))
31
+ : null}
16
32
  {order.discounts?.length > 0
17
33
  ? order.discounts.map((discount, index) => (
18
34
  <TableRow key={`${discount.description}-${index}`}>
@@ -0,0 +1,61 @@
1
+ import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
2
+ import { Badge } from '@/vdb/components/ui/badge.js';
3
+ import { api } from '@/vdb/graphql/api.js';
4
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
5
+ import { useQuery } from '@tanstack/react-query';
6
+ import { sellerOrdersDocument } from '../orders.graphql.js';
7
+ import { getSeller } from '../utils/order-utils.js';
8
+
9
+ export interface SellerOrdersCardProps {
10
+ orderId: string;
11
+ }
12
+
13
+ export function SellerOrdersCard({ orderId }: Readonly<SellerOrdersCardProps>) {
14
+ const { formatCurrency } = useLocalFormat();
15
+ const { data, isLoading, error } = useQuery({
16
+ queryKey: ['seller-orders', orderId],
17
+ queryFn: () => api.query(sellerOrdersDocument, { orderId }),
18
+ });
19
+
20
+ if (isLoading) {
21
+ return (
22
+ <div className="animate-pulse space-y-2">
23
+ {Array.from({ length: 2 }).map((_, i) => (
24
+ <div key={i} className="h-16 bg-muted rounded-md" />
25
+ ))}
26
+ </div>
27
+ );
28
+ }
29
+
30
+ if (error || !data?.order || !data.order.sellerOrders?.length) {
31
+ return null;
32
+ }
33
+
34
+ return (
35
+ <div className="flex flex-col gap-4 divide-y">
36
+ {data.order.sellerOrders.map(sellerOrder => {
37
+ const seller = getSeller(sellerOrder);
38
+
39
+ return (
40
+ <div key={sellerOrder.id} className="p-3 -mx-3 pb-6 transition-colors">
41
+ <div className="flex justify-between items-center">
42
+ <DetailPageButton
43
+ label={sellerOrder.code}
44
+ href={`/orders/${orderId}/seller-orders/${sellerOrder.id}`}
45
+ />
46
+ <div className="text-sm font-medium">
47
+ {formatCurrency(sellerOrder.totalWithTax, sellerOrder.currencyCode)}
48
+ </div>
49
+ </div>
50
+ <div className="flex justify-between mt-1">
51
+ <div className="flex gap-2">
52
+ {seller && <Badge variant={'secondary'}>{seller.name}</Badge>}
53
+ </div>
54
+ <Badge variant={'secondary'}>{sellerOrder.state}</Badge>
55
+ </div>
56
+ </div>
57
+ );
58
+ })}
59
+ </div>
60
+ );
61
+ }
@@ -1,13 +1,20 @@
1
+ import { Alert, AlertDescription } from '@/vdb/components/ui/alert.js';
2
+ import { Button } from '@/vdb/components/ui/button.js';
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@/vdb/components/ui/dialog.js';
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
1
12
  import { api } from '@/vdb/graphql/api.js';
2
- import { useMutation, useQuery } from '@tanstack/react-query';
3
- import { orderHistoryDocument, transitionOrderToStateDocument } from '../orders.graphql.js';
4
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/vdb/components/ui/dialog.js';
5
13
  import { Trans } from '@/vdb/lib/trans.js';
6
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
7
- import { useState } from 'react';
8
- import { Button } from '@/vdb/components/ui/button.js';
9
- import { Alert, AlertDescription } from '@/vdb/components/ui/alert.js';
14
+ import { useMutation, useQuery } from '@tanstack/react-query';
10
15
  import { ResultOf } from 'gql.tada';
16
+ import { useState } from 'react';
17
+ import { orderHistoryDocument, transitionOrderToStateDocument } from '../orders.graphql.js';
11
18
 
12
19
  /**
13
20
  * Returns the state the order was in before it entered 'Modifying'.
@@ -29,7 +36,7 @@ export function useTransitionOrderToState(orderId: string | undefined) {
29
36
  });
30
37
  const items = result.order?.history?.items ?? [];
31
38
  const modifyingEntry = items.find(i => i.data?.to === 'Modifying');
32
- return modifyingEntry ? (modifyingEntry.data?.from as string | undefined) : undefined;
39
+ return modifyingEntry ? (modifyingEntry.data?.from as string | undefined) : '';
33
40
  },
34
41
  enabled: !!orderId,
35
42
  });
@@ -48,7 +55,7 @@ export function useTransitionOrderToState(orderId: string | undefined) {
48
55
  }
49
56
  }
50
57
  return undefined;
51
- }
58
+ };
52
59
 
53
60
  const transitionToPreModifyingState = async () => {
54
61
  if (data && orderId) {
@@ -70,7 +77,7 @@ export function useTransitionOrderToState(orderId: string | undefined) {
70
77
  onSuccessFn?.();
71
78
  }
72
79
  },
73
- onError: (error) => {
80
+ onError: error => {
74
81
  setTransitionError(error.message);
75
82
  },
76
83
  });
@@ -189,6 +189,13 @@ export const orderDetailFragment = graphql(
189
189
  code
190
190
  }
191
191
  }
192
+ channels {
193
+ id
194
+ code
195
+ seller {
196
+ name
197
+ }
198
+ }
192
199
  code
193
200
  state
194
201
  nextStates
@@ -722,3 +729,27 @@ export const setOrderCustomFieldsDocument = graphql(`
722
729
  }
723
730
  }
724
731
  `);
732
+
733
+ export const sellerOrdersDocument = graphql(`
734
+ query GetSellerOrders($orderId: ID!) {
735
+ order(id: $orderId) {
736
+ id
737
+ sellerOrders {
738
+ id
739
+ code
740
+ state
741
+ orderPlacedAt
742
+ currencyCode
743
+ totalWithTax
744
+ channels {
745
+ id
746
+ code
747
+ seller {
748
+ id
749
+ name
750
+ }
751
+ }
752
+ }
753
+ }
754
+ }
755
+ `);
@@ -0,0 +1,50 @@
1
+ import { ErrorPage } from '@/vdb/components/shared/error-page.js';
2
+ import { Badge } from '@/vdb/components/ui/badge.js';
3
+ import { Button } from '@/vdb/components/ui/button.js';
4
+ import { Trans } from '@/vdb/lib/trans.js';
5
+ import { createFileRoute, Link } from '@tanstack/react-router';
6
+ import { ArrowLeft } from 'lucide-react';
7
+ import { OrderDetail, OrderDetailShared } from './components/order-detail-shared.js';
8
+ import { loadSellerOrder } from './utils/order-detail-loaders.js';
9
+ import { getSeller } from './utils/order-utils.js';
10
+
11
+ export const Route = createFileRoute(
12
+ '/_authenticated/_orders/orders_/$aggregateOrderId_/seller-orders/$sellerOrderId',
13
+ )({
14
+ component: SellerOrderDetailPage,
15
+ loader: ({ context, params }) => loadSellerOrder(context, params),
16
+ errorComponent: ({ error }) => <ErrorPage message={error.message} />,
17
+ });
18
+
19
+ function SellerOrderDetailPage() {
20
+ const params = Route.useParams();
21
+
22
+ const titleSlot = (order: OrderDetail) => {
23
+ const seller = getSeller(order);
24
+ return (
25
+ <div className="flex items-center gap-2">
26
+ <Button variant="ghost" size="sm" asChild>
27
+ <Link
28
+ to="/orders/$aggregateOrderId"
29
+ params={{ aggregateOrderId: params.aggregateOrderId }}
30
+ >
31
+ <ArrowLeft className="h-4 w-4" />
32
+ </Link>
33
+ </Button>
34
+ {order.code ?? ''}
35
+ <Badge variant="secondary">
36
+ <Trans>Seller order</Trans>
37
+ </Badge>
38
+ {seller && <Badge variant="outline">{seller.name}</Badge>}
39
+ </div>
40
+ );
41
+ };
42
+
43
+ return (
44
+ <OrderDetailShared
45
+ pageId="seller-order-detail"
46
+ orderId={params.sellerOrderId}
47
+ titleSlot={titleSlot}
48
+ />
49
+ );
50
+ }
@@ -1,303 +1,30 @@
1
- import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
2
1
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
3
- import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
4
- import { Button } from '@/vdb/components/ui/button.js';
5
- import { DropdownMenuItem } from '@/vdb/components/ui/dropdown-menu.js';
6
- import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
7
- import {
8
- Page,
9
- PageActionBar,
10
- PageActionBarRight,
11
- PageBlock,
12
- PageLayout,
13
- PageTitle,
14
- } from '@/vdb/framework/layout-engine/page-layout.js';
15
- import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
16
- import { api } from '@/vdb/graphql/api.js';
17
- import { ResultOf } from '@/vdb/graphql/graphql.js';
18
- import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
19
- import { Trans, useLingui } from '@/vdb/lib/trans.js';
20
- import { useMutation, useQueryClient } from '@tanstack/react-query';
21
- import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
22
- import { Pencil, User } from 'lucide-react';
23
- import { useMemo } from 'react';
24
- import { toast } from 'sonner';
25
- import { AddManualPaymentDialog } from './components/add-manual-payment-dialog.js';
26
- import { FulfillOrderDialog } from './components/fulfill-order-dialog.js';
27
- import { FulfillmentDetails } from './components/fulfillment-details.js';
28
- import { OrderAddress } from './components/order-address.js';
29
- import { OrderHistoryContainer } from './components/order-history/order-history-container.js';
30
- import { orderHistoryQueryKey } from './components/order-history/use-order-history.js';
31
- import { OrderTable } from './components/order-table.js';
32
- import { OrderTaxSummary } from './components/order-tax-summary.js';
33
- import { PaymentDetails } from './components/payment-details.js';
34
- import { getTypeForState, StateTransitionControl } from './components/state-transition-control.js';
35
- import { useTransitionOrderToState } from './components/use-transition-order-to-state.js';
36
- import {
37
- orderDetailDocument,
38
- setOrderCustomFieldsDocument,
39
- transitionOrderToStateDocument,
40
- } from './orders.graphql.js';
41
- import { canAddFulfillment, shouldShowAddManualPaymentButton } from './utils/order-utils.js';
42
-
43
- const pageId = 'order-detail';
2
+ import { PageBlock } from '@/vdb/framework/layout-engine/page-layout.js';
3
+ import { Trans } from '@/vdb/lib/trans.js';
4
+ import { createFileRoute } from '@tanstack/react-router';
5
+ import { OrderDetailShared } from './components/order-detail-shared.js';
6
+ import { SellerOrdersCard } from './components/seller-orders-card.js';
7
+ import { loadRegularOrder } from './utils/order-detail-loaders.js';
44
8
 
45
9
  export const Route = createFileRoute('/_authenticated/_orders/orders_/$id')({
46
10
  component: OrderDetailPage,
47
- loader: async ({ context, params }) => {
48
- if (!params.id) {
49
- throw new Error('ID param is required');
50
- }
51
-
52
- const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
53
- getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
54
- { id: params.id },
55
- );
56
-
57
- if (!result.order) {
58
- throw new Error(`Order with the ID ${params.id} was not found`);
59
- }
60
-
61
- if (result.order.state === 'Draft') {
62
- throw redirect({
63
- to: `/orders/draft/${params.id}`,
64
- });
65
- }
66
-
67
- if (result.order.state === 'Modifying') {
68
- throw redirect({
69
- to: `/orders/${params.id}/modify`,
70
- });
71
- }
72
-
73
- return {
74
- breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, result.order.code],
75
- };
76
- },
11
+ loader: ({ context, params }) => loadRegularOrder(context, params),
77
12
  errorComponent: ({ error }) => <ErrorPage message={error.message} />,
78
13
  });
79
14
 
80
15
  function OrderDetailPage() {
81
16
  const params = Route.useParams();
82
- const { i18n } = useLingui();
83
- const navigate = useNavigate();
84
- const queryClient = useQueryClient();
85
- const { form, submitHandler, entity, refreshEntity } = useDetailPage({
86
- pageId,
87
- queryDocument: orderDetailDocument,
88
- updateDocument: setOrderCustomFieldsDocument,
89
- setValuesForUpdate: (entity: any) => {
90
- return {
91
- id: entity.id,
92
- customFields: entity.customFields,
93
- };
94
- },
95
- params: { id: params.id },
96
- onSuccess: async () => {
97
- toast(i18n.t('Successfully updated order'));
98
- form.reset(form.getValues());
99
- },
100
- onError: err => {
101
- toast(i18n.t('Failed to update order'), {
102
- description: err instanceof Error ? err.message : 'Unknown error',
103
- });
104
- },
105
- });
106
- const { transitionToState } = useTransitionOrderToState(entity?.id);
107
- const transitionOrderToStateMutation = useMutation({
108
- mutationFn: api.mutate(transitionOrderToStateDocument),
109
- });
110
- const customFieldConfig = useCustomFieldConfig('Order');
111
- const stateTransitionActions = useMemo(() => {
112
- if (!entity) {
113
- return [];
114
- }
115
- return entity.nextStates.map(state => ({
116
- label: `Transition to ${state}`,
117
- type: getTypeForState(state),
118
- onClick: async () => {
119
- const transitionError = await transitionToState(state);
120
- if (transitionError) {
121
- toast(i18n.t('Failed to transition order to state'), {
122
- description: transitionError,
123
- });
124
- } else {
125
- refreshOrderAndHistory();
126
- }
127
- },
128
- }));
129
- }, [entity, transitionToState, i18n]);
130
-
131
- if (!entity) {
132
- return null;
133
- }
134
-
135
- const handleModifyClick = async () => {
136
- try {
137
- await transitionOrderToStateMutation.mutateAsync({
138
- id: entity.id,
139
- state: 'Modifying',
140
- });
141
- const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
142
- await queryClient.invalidateQueries({ queryKey });
143
- await navigate({ to: `/orders/$id/modify`, params: { id: entity.id } });
144
- } catch (error) {
145
- toast(i18n.t('Failed to modify order'), {
146
- description: error instanceof Error ? error.message : 'Unknown error',
147
- });
148
- }
149
- };
150
-
151
- const nextStates = entity.nextStates;
152
- const showAddPaymentButton = shouldShowAddManualPaymentButton(entity);
153
- const showFulfillButton = canAddFulfillment(entity);
154
-
155
- async function refreshOrderAndHistory() {
156
- if (entity) {
157
- const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
158
- await queryClient.invalidateQueries({ queryKey });
159
- queryClient.refetchQueries({ queryKey: orderHistoryQueryKey(entity.id) });
160
- }
161
- }
162
-
163
17
  return (
164
- <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
165
- <PageTitle>{entity?.code ?? ''}</PageTitle>
166
- <PageActionBar>
167
- <PageActionBarRight
168
- dropdownMenuItems={[
169
- ...(nextStates.includes('Modifying')
170
- ? [
171
- {
172
- component: () => (
173
- <DropdownMenuItem onClick={handleModifyClick}>
174
- <Pencil className="w-4 h-4" />
175
- <Trans>Modify</Trans>
176
- </DropdownMenuItem>
177
- ),
178
- },
179
- ]
180
- : []),
181
- ]}
182
- >
183
- {showAddPaymentButton && (
184
- <PermissionGuard requires={['UpdateOrder']}>
185
- <AddManualPaymentDialog
186
- order={entity}
187
- onSuccess={() => {
188
- refreshEntity();
189
- }}
190
- />
191
- </PermissionGuard>
192
- )}
193
- {showFulfillButton && (
194
- <PermissionGuard requires={['UpdateOrder']}>
195
- <FulfillOrderDialog
196
- order={entity}
197
- onSuccess={() => {
198
- refreshOrderAndHistory();
199
- }}
200
- />
201
- </PermissionGuard>
202
- )}
203
- </PageActionBarRight>
204
- </PageActionBar>
205
- <PageLayout>
206
- <PageBlock column="main" blockId="order-table">
207
- <OrderTable order={entity} pageId={pageId} />
208
- </PageBlock>
209
- <PageBlock column="main" blockId="tax-summary" title={<Trans>Tax summary</Trans>}>
210
- <OrderTaxSummary order={entity} />
211
- </PageBlock>
212
- {customFieldConfig?.length ? (
213
- <PageBlock column="main" blockId="custom-fields">
214
- <CustomFieldsForm entityType="Order" control={form.control} />
215
- <div className="flex justify-end">
216
- <Button
217
- type="submit"
218
- disabled={!form.formState.isDirty || !form.formState.isValid}
219
- >
220
- Save
221
- </Button>
222
- </div>
18
+ <OrderDetailShared
19
+ pageId="order-detail"
20
+ orderId={params.id}
21
+ beforeOrderTable={order =>
22
+ order.sellerOrders?.length ? (
23
+ <PageBlock column="main" blockId="seller-orders" title={<Trans>Seller orders</Trans>}>
24
+ <SellerOrdersCard orderId={params.id} />
223
25
  </PageBlock>
224
- ) : null}
225
- <PageBlock column="main" blockId="payment-details" title={<Trans>Payment details</Trans>}>
226
- <div className="grid lg:grid-cols-2 gap-4">
227
- {entity?.payments?.map(payment => (
228
- <PaymentDetails
229
- key={payment.id}
230
- payment={payment}
231
- currencyCode={entity.currencyCode}
232
- onSuccess={() => refreshOrderAndHistory()}
233
- />
234
- ))}
235
- </div>
236
- </PageBlock>
237
- <PageBlock column="main" blockId="order-history" title={<Trans>Order history</Trans>}>
238
- <OrderHistoryContainer orderId={entity.id} />
239
- </PageBlock>
240
- <PageBlock column="side" blockId="state">
241
- <StateTransitionControl
242
- currentState={entity?.state}
243
- actions={stateTransitionActions}
244
- isLoading={transitionOrderToStateMutation.isPending}
245
- />
246
- </PageBlock>
247
- <PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
248
- <Button variant="ghost" asChild>
249
- <Link to={`/customers/${entity?.customer?.id}`}>
250
- <User className="w-4 h-4" />
251
- {entity?.customer?.firstName} {entity?.customer?.lastName}
252
- </Link>
253
- </Button>
254
- <div className="mt-4 divide-y">
255
- {entity?.shippingAddress && (
256
- <div className="pb-6">
257
- <div className="font-medium">
258
- <Trans>Shipping address</Trans>
259
- </div>
260
- <OrderAddress address={entity.shippingAddress} />
261
- </div>
262
- )}
263
- {entity?.billingAddress && (
264
- <div className="pt-4">
265
- <div className="font-medium">
266
- <Trans>Billing address</Trans>
267
- </div>
268
- <OrderAddress address={entity.billingAddress} />
269
- </div>
270
- )}
271
- </div>
272
- </PageBlock>
273
- <PageBlock
274
- column="side"
275
- blockId="fulfillment-details"
276
- title={<Trans>Fulfillment details</Trans>}
277
- >
278
- {entity?.fulfillments?.length && entity.fulfillments.length > 0 ? (
279
- <div className="space-y-2">
280
- {entity?.fulfillments?.map(fulfillment => (
281
- <FulfillmentDetails
282
- key={fulfillment.id}
283
- order={entity}
284
- fulfillment={fulfillment}
285
- onSuccess={() => {
286
- refreshEntity();
287
- queryClient.refetchQueries({
288
- queryKey: orderHistoryQueryKey(entity.id),
289
- });
290
- }}
291
- />
292
- ))}
293
- </div>
294
- ) : (
295
- <div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
296
- <Trans>No fulfillments</Trans>
297
- </div>
298
- )}
299
- </PageBlock>
300
- </PageLayout>
301
- </Page>
26
+ ) : undefined
27
+ }
28
+ />
302
29
  );
303
30
  }
@@ -1,6 +1,5 @@
1
1
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
2
2
  import { Button } from '@/vdb/components/ui/button.js';
3
- import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
4
3
  import {
5
4
  Page,
6
5
  PageActionBar,
@@ -13,8 +12,8 @@ import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-d
13
12
  import { api } from '@/vdb/graphql/api.js';
14
13
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
15
14
  import { useQuery, useQueryClient } from '@tanstack/react-query';
16
- import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
17
- import { ResultOf, VariablesOf } from 'gql.tada';
15
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
16
+ import { VariablesOf } from 'gql.tada';
18
17
  import { User } from 'lucide-react';
19
18
  import { useEffect, useState } from 'react';
20
19
  import { toast } from 'sonner';
@@ -29,6 +28,7 @@ import {
29
28
  modifyOrderDocument,
30
29
  orderDetailDocument,
31
30
  } from './orders.graphql.js';
31
+ import { loadModifyingOrder } from './utils/order-detail-loaders.js';
32
32
  import { AddressFragment, Order } from './utils/order-types.js';
33
33
 
34
34
  const pageId = 'order-modify';
@@ -36,39 +36,7 @@ type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
36
36
 
37
37
  export const Route = createFileRoute('/_authenticated/_orders/orders_/$id_/modify')({
38
38
  component: ModifyOrderPage,
39
- loader: async ({ context, params }) => {
40
- if (!params.id) {
41
- throw new Error('ID param is required');
42
- }
43
-
44
- const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
45
- getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
46
- { id: params.id },
47
- );
48
-
49
- if (!result.order) {
50
- throw new Error(`Order with the ID ${params.id} was not found`);
51
- }
52
-
53
- if (result.order.state === 'Draft') {
54
- throw redirect({
55
- to: `/orders/draft/${params.id}`,
56
- });
57
- }
58
- if (result.order.state !== 'Modifying') {
59
- throw redirect({
60
- to: `/orders/${params.id}`,
61
- });
62
- }
63
-
64
- return {
65
- breadcrumb: [
66
- { path: '/orders', label: <Trans>Orders</Trans> },
67
- result.order.code,
68
- { label: <Trans>Modify</Trans> },
69
- ],
70
- };
71
- },
39
+ loader: async ({ context, params }) => loadModifyingOrder(context, params),
72
40
  errorComponent: ({ error }) => <ErrorPage message={error.message} />,
73
41
  });
74
42
 
@@ -385,10 +353,10 @@ function ModifyOrderPage() {
385
353
  }
386
354
 
387
355
  // On successful state transition, invalidate the order detail query and navigate to the order detail page
388
- const onSuccess = () => {
356
+ const onSuccess = async () => {
389
357
  const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
390
- queryClient.invalidateQueries({ queryKey });
391
- navigate({ to: `/orders/$id`, params: { id: entity?.id } });
358
+ await queryClient.invalidateQueries({ queryKey });
359
+ await navigate({ to: `/orders/$id`, params: { id: entity?.id } });
392
360
  };
393
361
 
394
362
  const handleCancelModificationClick = async () => {