@vendure/dashboard 3.2.3 → 3.3.0

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 (123) hide show
  1. package/dist/plugin/utils/ast-utils.d.ts +10 -0
  2. package/dist/plugin/utils/ast-utils.js +96 -0
  3. package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
  4. package/dist/plugin/utils/ast-utils.spec.js +120 -0
  5. package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
  6. package/dist/plugin/utils/config-loader.js +325 -0
  7. package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
  8. package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +7 -1
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
  10. package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
  11. package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
  12. package/dist/plugin/vite-plugin-config-loader.js +18 -9
  13. package/dist/plugin/vite-plugin-config.js +4 -6
  14. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  15. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  16. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  17. package/package.json +16 -11
  18. package/src/app/app-providers.tsx +9 -9
  19. package/src/app/main.tsx +1 -1
  20. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  21. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  22. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  23. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  24. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  25. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  26. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  27. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  30. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  31. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
  32. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  33. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  34. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  35. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
  36. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  37. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
  38. package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
  39. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
  40. package/src/app/routes/_authenticated.tsx +12 -1
  41. package/src/app/styles.css +15 -0
  42. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  43. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  44. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  45. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  46. package/src/lib/components/data-table/data-table-types.ts +1 -0
  47. package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
  48. package/src/lib/components/data-table/data-table.tsx +49 -44
  49. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  50. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  51. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  52. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  53. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  54. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  55. package/src/lib/components/data-table/refresh-button.tsx +25 -0
  56. package/src/lib/components/layout/nav-user.tsx +20 -15
  57. package/src/lib/components/layout/prerelease-popup.tsx +1 -5
  58. package/src/lib/components/shared/alerts.tsx +19 -1
  59. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  60. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  61. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  62. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  63. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  64. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  65. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  66. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  67. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  68. package/src/lib/components/shared/customer-selector.tsx +13 -14
  69. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  70. package/src/lib/components/shared/entity-assets.tsx +3 -3
  71. package/src/lib/components/shared/error-page.tsx +2 -2
  72. package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
  73. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
  74. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  75. package/src/lib/components/shared/vendure-image.tsx +1 -1
  76. package/src/lib/components/ui/calendar.tsx +508 -63
  77. package/src/lib/framework/alert/alert-extensions.tsx +31 -0
  78. package/src/lib/framework/alert/alert-item.tsx +47 -0
  79. package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
  80. package/src/lib/framework/alert/types.ts +13 -0
  81. package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
  82. package/src/lib/framework/defaults.ts +34 -0
  83. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  84. package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
  85. package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
  86. package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
  87. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  88. package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
  89. package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
  90. package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
  91. package/src/lib/framework/page/detail-page.tsx +62 -9
  92. package/src/lib/framework/page/list-page.tsx +42 -4
  93. package/src/lib/framework/page/page-api.ts +1 -1
  94. package/src/lib/framework/page/use-detail-page.ts +82 -0
  95. package/src/lib/framework/registry/registry-types.ts +6 -2
  96. package/src/lib/graphql/fragments.tsx +8 -0
  97. package/src/lib/graphql/graphql-env.d.ts +25 -9
  98. package/src/lib/hooks/use-auth.tsx +13 -1
  99. package/src/lib/hooks/use-channel.ts +13 -0
  100. package/src/lib/hooks/use-local-format.ts +28 -1
  101. package/src/lib/hooks/use-page.tsx +2 -3
  102. package/src/lib/hooks/use-permissions.ts +13 -0
  103. package/src/lib/index.ts +7 -8
  104. package/src/lib/providers/auth.tsx +22 -9
  105. package/src/lib/providers/channel-provider.tsx +9 -1
  106. package/src/lib/providers/server-config.tsx +7 -1
  107. package/src/lib/providers/user-settings.tsx +24 -0
  108. package/vite/utils/ast-utils.spec.ts +128 -0
  109. package/vite/utils/ast-utils.ts +119 -0
  110. package/vite/utils/config-loader.ts +410 -0
  111. package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
  112. package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
  113. package/vite/vite-plugin-admin-api-schema.ts +2 -12
  114. package/vite/vite-plugin-config-loader.ts +25 -13
  115. package/vite/vite-plugin-config.ts +1 -0
  116. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  117. package/vite/vite-plugin-gql-tada.ts +2 -2
  118. package/vite/vite-plugin-ui-config.ts +3 -2
  119. package/dist/plugin/config-loader.js +0 -141
  120. package/src/lib/components/shared/asset-preview.tsx +0 -345
  121. package/src/lib/components/ui/avatar.tsx +0 -38
  122. package/vite/config-loader.ts +0 -181
  123. /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
@@ -0,0 +1,418 @@
1
+ import { ConfirmationDialog } from '@/components/shared/confirmation-dialog.js';
2
+ import { CustomerSelector } from '@/components/shared/customer-selector.js';
3
+ import { ErrorPage } from '@/components/shared/error-page.js';
4
+ import { PermissionGuard } from '@/components/shared/permission-guard.js';
5
+ import { Button } from '@/components/ui/button.js';
6
+ import { Form } from '@/components/ui/form.js';
7
+ import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
8
+ import { useGeneratedForm } from '@/framework/form-engine/use-generated-form.js';
9
+ import { CustomFieldsPageBlock, Page, PageActionBar, PageActionBarRight, PageBlock, PageLayout, PageTitle } from '@/framework/layout-engine/page-layout.js';
10
+ import { getDetailQueryOptions, useDetailPage } from '@/framework/page/use-detail-page.js';
11
+ import { api } from '@/graphql/api.js';
12
+ import { Trans, useLingui } from '@/lib/trans.js';
13
+ import { useMutation, useQuery } from '@tanstack/react-query';
14
+ import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
15
+ import { ResultOf } from 'gql.tada';
16
+ import { User } from 'lucide-react';
17
+ import { toast } from 'sonner';
18
+ import { CustomerAddressSelector } from './components/customer-address-selector.js';
19
+ import { EditOrderTable } from './components/edit-order-table.js';
20
+ import { OrderAddress } from './components/order-address.js';
21
+ import { addItemToDraftOrderDocument, adjustDraftOrderLineDocument, applyCouponCodeToDraftOrderDocument, deleteDraftOrderDocument, draftOrderEligibleShippingMethodsDocument, orderDetailDocument, removeCouponCodeFromDraftOrderDocument, removeDraftOrderLineDocument, setBillingAddressForDraftOrderDocument, setCustomerForDraftOrderDocument, setDraftOrderCustomFieldsDocument, setDraftOrderShippingMethodDocument, setShippingAddressForDraftOrderDocument, transitionOrderToStateDocument, unsetBillingAddressForDraftOrderDocument, unsetShippingAddressForDraftOrderDocument } from './orders.graphql.js';
22
+ import { CustomFieldsForm } from '@/components/shared/custom-fields-form.js';
23
+
24
+ export const Route = createFileRoute('/_authenticated/_orders/orders_/draft/$id')({
25
+ component: DraftOrderPage,
26
+ loader: async ({
27
+ context,
28
+ params,
29
+ }) => {
30
+ if (!params.id) {
31
+ throw new Error('ID param is required');
32
+ }
33
+
34
+ const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
35
+ getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
36
+ { id: params.id },
37
+ );
38
+
39
+ if (!result.order) {
40
+ throw new Error(`Order with the ID ${params.id} was not found`);
41
+ }
42
+
43
+ if (result.order.state !== 'Draft') {
44
+ throw redirect({
45
+ to: `/orders/${params.id}`,
46
+ });
47
+ }
48
+
49
+ return {
50
+ breadcrumb: [{ path: '/orders', label: 'Orders' }, result.order.code],
51
+ };
52
+ },
53
+ errorComponent: ({ error }) => <ErrorPage message={error.message} />,
54
+ });
55
+
56
+ function DraftOrderPage() {
57
+ const params = Route.useParams();
58
+ const { i18n } = useLingui();
59
+ const navigate = useNavigate();
60
+
61
+ const { entity, refreshEntity, form } = useDetailPage({
62
+ queryDocument: addCustomFields(orderDetailDocument),
63
+ setValuesForUpdate: entity => {
64
+ return {
65
+ id: entity.id,
66
+ customFields: entity.customFields,
67
+ };
68
+ },
69
+ params: { id: params.id },
70
+ });
71
+
72
+ const { form: orderLineForm } = useGeneratedForm({
73
+ document: addCustomFields(adjustDraftOrderLineDocument),
74
+ varName: undefined,
75
+ entity: entity?.lines[0],
76
+ setValues: entity => {
77
+ return {
78
+ orderId: entity.id,
79
+ input: {
80
+ quantity: entity.quantity,
81
+ orderLineId: entity.id,
82
+ customFields: entity.customFields,
83
+ }
84
+ };
85
+ },
86
+ });
87
+
88
+ const { form: orderCustomFieldsForm } = useGeneratedForm({
89
+ document: setDraftOrderCustomFieldsDocument,
90
+ varName: undefined,
91
+ entity: entity,
92
+ setValues: entity => {
93
+ return {
94
+ orderId: entity.id,
95
+ input: {
96
+ id: entity.id,
97
+ customFields: entity.customFields,
98
+ }
99
+ };
100
+ },
101
+ });
102
+
103
+ const { mutate: setDraftOrderCustomFields } = useMutation({
104
+ mutationFn: api.mutate(setDraftOrderCustomFieldsDocument),
105
+ onSuccess: (result: ResultOf<typeof setDraftOrderCustomFieldsDocument>) => {
106
+ refreshEntity();
107
+ },
108
+ });
109
+
110
+ const { data: eligibleShippingMethods } = useQuery({
111
+ queryKey: ['eligibleShippingMethods', entity?.id],
112
+ queryFn: () => api.query(draftOrderEligibleShippingMethodsDocument, { orderId: entity?.id ?? '' }),
113
+ enabled: !!entity?.shippingAddress?.streetLine1,
114
+ });
115
+
116
+ const { mutate: addItemToDraftOrder } = useMutation({
117
+ mutationFn: api.mutate(addItemToDraftOrderDocument),
118
+ onSuccess: (result: ResultOf<typeof addItemToDraftOrderDocument>) => {
119
+ const order = result.addItemToDraftOrder;
120
+ switch (order.__typename) {
121
+ case 'Order':
122
+ toast.success(i18n.t('Item added to order'));
123
+ refreshEntity();
124
+ break;
125
+ default:
126
+ toast.error(order.message);
127
+ break;
128
+ }
129
+ },
130
+ });
131
+
132
+ const { mutate: adjustDraftOrderLine } = useMutation({
133
+ mutationFn: api.mutate(adjustDraftOrderLineDocument),
134
+ onSuccess: (result: ResultOf<typeof adjustDraftOrderLineDocument>) => {
135
+ const order = result.adjustDraftOrderLine;
136
+ switch (order.__typename) {
137
+ case 'Order':
138
+ toast.success(i18n.t('Order line updated'));
139
+ refreshEntity();
140
+ break;
141
+ default:
142
+ toast.error(order.message);
143
+ break;
144
+ }
145
+ },
146
+ });
147
+
148
+ const { mutate: removeDraftOrderLine } = useMutation({
149
+ mutationFn: api.mutate(removeDraftOrderLineDocument),
150
+ onSuccess: (result: ResultOf<typeof removeDraftOrderLineDocument>) => {
151
+ const order = result.removeDraftOrderLine;
152
+ switch (order.__typename) {
153
+ case 'Order':
154
+ toast.success(i18n.t('Order line removed'));
155
+ refreshEntity();
156
+ break;
157
+ default:
158
+ toast.error(order.message);
159
+ break;
160
+ }
161
+ },
162
+ });
163
+
164
+ const { mutate: setCustomerForDraftOrder } = useMutation({
165
+ mutationFn: api.mutate(setCustomerForDraftOrderDocument),
166
+ onSuccess: (result: ResultOf<typeof setCustomerForDraftOrderDocument>) => {
167
+ const order = result.setCustomerForDraftOrder;
168
+ switch (order.__typename) {
169
+ case 'Order':
170
+ toast.success(i18n.t('Customer set for order'));
171
+ refreshEntity();
172
+ break;
173
+ default:
174
+ toast.error(order.message);
175
+ break;
176
+ }
177
+ },
178
+ });
179
+
180
+ const { mutate: setShippingAddressForDraftOrder } = useMutation({
181
+ mutationFn: api.mutate(setShippingAddressForDraftOrderDocument),
182
+ onSuccess: (result: ResultOf<typeof setShippingAddressForDraftOrderDocument>) => {
183
+ toast.success(i18n.t('Shipping address set for order'));
184
+ refreshEntity();
185
+ },
186
+ });
187
+
188
+ const { mutate: setBillingAddressForDraftOrder } = useMutation({
189
+ mutationFn: api.mutate(setBillingAddressForDraftOrderDocument),
190
+ onSuccess: (result: ResultOf<typeof setBillingAddressForDraftOrderDocument>) => {
191
+ toast.success(i18n.t('Billing address set for order'));
192
+ refreshEntity();
193
+ },
194
+ });
195
+
196
+ const { mutate: unsetShippingAddressForDraftOrder } = useMutation({
197
+ mutationFn: api.mutate(unsetShippingAddressForDraftOrderDocument),
198
+ onSuccess: (result: ResultOf<typeof unsetShippingAddressForDraftOrderDocument>) => {
199
+ toast.success(i18n.t('Shipping address unset for order'));
200
+ refreshEntity();
201
+ },
202
+ });
203
+
204
+ const { mutate: unsetBillingAddressForDraftOrder } = useMutation({
205
+ mutationFn: api.mutate(unsetBillingAddressForDraftOrderDocument),
206
+ onSuccess: (result: ResultOf<typeof unsetBillingAddressForDraftOrderDocument>) => {
207
+ toast.success(i18n.t('Billing address unset for order'));
208
+ refreshEntity();
209
+ },
210
+ });
211
+
212
+ const { mutate: setShippingMethodForDraftOrder } = useMutation({
213
+ mutationFn: api.mutate(setDraftOrderShippingMethodDocument),
214
+ onSuccess: (result: ResultOf<typeof setDraftOrderShippingMethodDocument>) => {
215
+ const order = result.setDraftOrderShippingMethod;
216
+ switch (order.__typename) {
217
+ case 'Order':
218
+ toast.success(i18n.t('Shipping method set for order'));
219
+ refreshEntity();
220
+ break;
221
+ default:
222
+ toast.error(order.message);
223
+ break;
224
+ }
225
+ },
226
+ });
227
+
228
+ const { mutate: setCouponCodeForDraftOrder } = useMutation({
229
+ mutationFn: api.mutate(applyCouponCodeToDraftOrderDocument),
230
+ onSuccess: (result: ResultOf<typeof applyCouponCodeToDraftOrderDocument>) => {
231
+ const order = result.applyCouponCodeToDraftOrder;
232
+ switch (order.__typename) {
233
+ case 'Order':
234
+ toast.success(i18n.t('Coupon code set for order'));
235
+ refreshEntity();
236
+ break;
237
+ default:
238
+ toast.error(order.message);
239
+ break;
240
+ }
241
+ },
242
+ });
243
+
244
+ const { mutate: removeCouponCodeForDraftOrder } = useMutation({
245
+ mutationFn: api.mutate(removeCouponCodeFromDraftOrderDocument),
246
+ onSuccess: (result: ResultOf<typeof removeCouponCodeFromDraftOrderDocument>) => {
247
+ const order = result.removeCouponCodeFromDraftOrder;
248
+ toast.success(i18n.t('Coupon code removed from order'));
249
+ refreshEntity();
250
+ },
251
+ });
252
+
253
+ const { mutate: completeDraftOrder } = useMutation({
254
+ mutationFn: api.mutate(transitionOrderToStateDocument),
255
+ onSuccess: async (result: ResultOf<typeof transitionOrderToStateDocument>) => {
256
+ const order = result.transitionOrderToState;
257
+ switch (order?.__typename) {
258
+ case 'Order':
259
+ toast.success(i18n.t('Draft order completed'));
260
+ refreshEntity();
261
+ setTimeout(() => {
262
+ navigate({ to: `/orders/$id`, params: { id: order.id } });
263
+ }, 500);
264
+ break;
265
+ default:
266
+ toast.error(order ? order.message : 'Unknown error');
267
+ break;
268
+ }
269
+ },
270
+ });
271
+
272
+ const { mutate: deleteDraftOrder } = useMutation({
273
+ mutationFn: api.mutate(deleteDraftOrderDocument),
274
+ onSuccess: (result: ResultOf<typeof deleteDraftOrderDocument>) => {
275
+ if (result.deleteDraftOrder.result === 'DELETED') {
276
+ toast.success(i18n.t('Draft order deleted'));
277
+ navigate({ to: '/orders' });
278
+ } else {
279
+ toast.error(result.deleteDraftOrder.message);
280
+ }
281
+ },
282
+ });
283
+
284
+ if (!entity) {
285
+ return null;
286
+ }
287
+
288
+ const onSaveCustomFields = (values: any) => {
289
+ setDraftOrderCustomFields({ input: { id: entity.id, customFields: values.input?.customFields }, orderId: entity.id });
290
+ }
291
+
292
+ return (
293
+ <Page pageId="draft-order-detail" form={form}>
294
+ <PageTitle><Trans>Draft order</Trans>: {entity?.code ?? ''}</PageTitle>
295
+ <PageActionBar>
296
+ <PageActionBarRight>
297
+ <PermissionGuard requires={['DeleteOrder']}>
298
+ <ConfirmationDialog
299
+ title={i18n.t('Delete draft order')}
300
+ description={i18n.t('Are you sure you want to delete this draft order?')}
301
+ onConfirm={() => {
302
+ deleteDraftOrder({ orderId: entity.id });
303
+ }}
304
+ >
305
+ <Button variant="destructive" type="button">
306
+ <Trans>Delete draft</Trans>
307
+ </Button>
308
+ </ConfirmationDialog>
309
+
310
+ </PermissionGuard>
311
+ <PermissionGuard requires={['UpdateOrder']}>
312
+ <Button type="button"
313
+ disabled={!entity.customer || entity.lines.length === 0 || entity.shippingLines.length === 0 || entity.state !== 'Draft'}
314
+ onClick={() => completeDraftOrder({ id: entity.id, state: 'ArrangingPayment' })}
315
+ >
316
+ <Trans>Complete draft</Trans>
317
+ </Button>
318
+ </PermissionGuard>
319
+ </PageActionBarRight>
320
+ </PageActionBar>
321
+ <PageLayout>
322
+ <PageBlock column="main" blockId="order-table">
323
+ <EditOrderTable order={entity}
324
+ eligibleShippingMethods={eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder ?? []}
325
+ onSetShippingMethod={(e) => setShippingMethodForDraftOrder({ orderId: entity.id, shippingMethodId: e.shippingMethodId })}
326
+ onAddItem={(e) => addItemToDraftOrder({ orderId: entity.id, input: { productVariantId: e.productVariantId, quantity: 1 } })}
327
+ onAdjustLine={(e) => adjustDraftOrderLine({ orderId: entity.id, input: { orderLineId: e.lineId, quantity: e.quantity, customFields: e.customFields } as any })}
328
+ onRemoveLine={(e) => removeDraftOrderLine({ orderId: entity.id, orderLineId: e.lineId })}
329
+ onApplyCouponCode={(e) => setCouponCodeForDraftOrder({ orderId: entity.id, couponCode: e.couponCode })}
330
+ onRemoveCouponCode={(e) => removeCouponCodeForDraftOrder({ orderId: entity.id, couponCode: e.couponCode })}
331
+ orderLineForm={orderLineForm}
332
+ />
333
+ </PageBlock>
334
+ <PageBlock column="main" blockId="order-custom-fields" title={<Trans>Custom fields</Trans>}>
335
+ <Form {...orderCustomFieldsForm}>
336
+ <CustomFieldsForm entityType="Order" control={orderCustomFieldsForm.control} formPathPrefix='input' />
337
+ <div className="mt-4">
338
+ <Button type="submit" className=""
339
+ disabled={!orderCustomFieldsForm.formState.isValid || !orderCustomFieldsForm.formState.isDirty}
340
+ onClick={(e) => {
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ orderCustomFieldsForm.handleSubmit(onSaveCustomFields)();
344
+ }}>
345
+ <Trans>Set custom fields</Trans>
346
+ </Button>
347
+ </div>
348
+ </Form>
349
+ </PageBlock>
350
+ <PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
351
+ {entity?.customer?.id ? <Button variant="ghost" asChild className="mb-4">
352
+ <Link to={`/customers/${entity?.customer?.id}`}>
353
+ <User className="w-4 h-4" />
354
+ {entity?.customer?.firstName} {entity?.customer?.lastName}
355
+ </Link>
356
+ </Button> : null}
357
+ <CustomerSelector onSelect={customer => {
358
+ setCustomerForDraftOrder({ orderId: entity.id, customerId: customer.id });
359
+ }} />
360
+ </PageBlock>
361
+ <PageBlock column="side" blockId="shipping-address" title={<Trans>Shipping address</Trans>}>
362
+ <div className="flex flex-col">
363
+ <OrderAddress address={entity.shippingAddress ?? undefined} />
364
+ {entity.shippingAddress?.streetLine1
365
+ ? <RemoveAddressButton onClick={() => unsetShippingAddressForDraftOrder({ orderId: entity.id })} />
366
+ : <CustomerAddressSelector customerId={entity.customer?.id} onSelect={address => {
367
+ setShippingAddressForDraftOrder({
368
+ orderId: entity.id, input: {
369
+ fullName: address.fullName,
370
+ company: address.company,
371
+ streetLine1: address.streetLine1,
372
+ streetLine2: address.streetLine2,
373
+ city: address.city,
374
+ province: address.province,
375
+ postalCode: address.postalCode,
376
+ countryCode: address.country.code,
377
+ phoneNumber: address.phoneNumber,
378
+ }
379
+ });
380
+ }} />
381
+ }
382
+ </div>
383
+ </PageBlock>
384
+ <PageBlock column="side" blockId="billing-address" title={<Trans>Billing address</Trans>}>
385
+ <div className="flex flex-col">
386
+ <OrderAddress address={entity.billingAddress ?? undefined} />
387
+ {entity.billingAddress?.streetLine1
388
+ ? <RemoveAddressButton onClick={() => unsetBillingAddressForDraftOrder({ orderId: entity.id })} />
389
+ : <CustomerAddressSelector customerId={entity.customer?.id} onSelect={address => {
390
+ setBillingAddressForDraftOrder({
391
+ orderId: entity.id, input: {
392
+ fullName: address.fullName,
393
+ company: address.company,
394
+ streetLine1: address.streetLine1,
395
+ streetLine2: address.streetLine2,
396
+ city: address.city,
397
+ province: address.province,
398
+ postalCode: address.postalCode,
399
+ countryCode: address.country.code,
400
+ phoneNumber: address.phoneNumber,
401
+ }
402
+ });
403
+ }} />
404
+ }
405
+ </div>
406
+ </PageBlock>
407
+ </PageLayout>
408
+ </Page>
409
+ );
410
+ }
411
+
412
+ function RemoveAddressButton(props: { onClick: () => void }) {
413
+ return (<div className="">
414
+ <Button variant="outline" className="mt-4" size="sm" onClick={props.onClick}>
415
+ <Trans>Remove</Trans>
416
+ </Button>
417
+ </div>)
418
+ }
@@ -200,7 +200,9 @@ function ProductVariantDetailPage() {
200
200
  <Trans>Stock level</Trans>
201
201
  </FormLabel>
202
202
  <FormControl>
203
- <Input type="number" {...field} />
203
+ <Input type="number" value={field.value} onChange={e => {
204
+ field.onChange(e.target.valueAsNumber);
205
+ }} />
204
206
  </FormControl>
205
207
  </FormItem>
206
208
  )}
@@ -224,7 +226,11 @@ function ProductVariantDetailPage() {
224
226
  <FormLabel>
225
227
  <Trans>Track inventory</Trans>
226
228
  </FormLabel>
227
- <Select onValueChange={field.onChange} value={field.value}>
229
+ <Select onValueChange={val => {
230
+ if (val) {
231
+ field.onChange(val)
232
+ }
233
+ }} value={field.value}>
228
234
  <FormControl>
229
235
  <SelectTrigger className="">
230
236
  <SelectValue placeholder="Track inventory" />
@@ -38,7 +38,7 @@ function ProductListPage() {
38
38
  <Button asChild>
39
39
  <Link to="./new">
40
40
  <PlusIcon className="mr-2 h-4 w-4" />
41
- New Product
41
+ <Trans>New Product</Trans>
42
42
  </Link>
43
43
  </Button>
44
44
  </PermissionGuard>
@@ -57,6 +57,8 @@ function PromotionDetailPage() {
57
57
  transformCreateInput: values => {
58
58
  return {
59
59
  ...values,
60
+ startsAt: values.startsAt || undefined,
61
+ endsAt: values.endsAt || undefined,
60
62
  conditions: values.conditions.filter(c => c.code !== ''),
61
63
  actions: values.actions.filter(a => a.code !== ''),
62
64
  };
@@ -98,6 +100,10 @@ function PromotionDetailPage() {
98
100
  if (creatingNewEntity) {
99
101
  await navigate({ to: `../${data.id}`, from: Route.id });
100
102
  }
103
+ } else {
104
+ toast.error(i18n.t('Failed to update promotion'), {
105
+ description: data.message,
106
+ });
101
107
  }
102
108
  },
103
109
  onError: err => {
@@ -1,14 +1,13 @@
1
+ import { Badge } from '@/components/ui/badge.js';
2
+ import { Button } from '@/components/ui/button.js';
1
3
  import { ListPage } from '@/framework/page/list-page.js';
4
+ import { api } from '@/graphql/api.js';
2
5
  import { Trans } from '@/lib/trans.js';
3
6
  import { createFileRoute } from '@tanstack/react-router';
4
- import { jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
5
- import { Badge } from '@/components/ui/badge.js';
6
- import { Button } from '@/components/ui/button.js';
7
+ import { formatRelative } from 'date-fns';
8
+ import { Ban, CheckCircle2Icon, CircleXIcon, ClockIcon, LoaderIcon, RotateCcw } from 'lucide-react';
7
9
  import { PayloadDialog } from './components/payload-dialog.js';
8
- import { differenceInMilliseconds, formatDuration, formatRelative } from 'date-fns';
9
- import { Ban, CircleXIcon, ClockIcon, LoaderIcon, RotateCcw } from 'lucide-react';
10
- import { CheckCircle2Icon } from 'lucide-react';
11
- import { api } from '@/graphql/api.js';
10
+ import { jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
12
11
 
13
12
  export const Route = createFileRoute('/_authenticated/_system/job-queue')({
14
13
  component: JobQueuePage,
@@ -58,7 +57,7 @@ function JobQueuePage() {
58
57
  customizeColumns={{
59
58
  createdAt: {
60
59
  header: 'Created At',
61
- cell: ({ row }) => formatRelative(row.original.createdAt, new Date()),
60
+ cell: ({ row }) => <div title={row.original.createdAt}>{formatRelative(new Date(row.original.createdAt), new Date())}</div>,
62
61
  },
63
62
  data: {
64
63
  header: 'Data',