@vendure/dashboard 3.2.2 → 3.2.4
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.
- package/dist/plugin/utils/ast-utils.d.ts +10 -0
- package/dist/plugin/utils/ast-utils.js +96 -0
- package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
- package/dist/plugin/utils/ast-utils.spec.js +120 -0
- package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
- package/dist/plugin/utils/config-loader.js +325 -0
- package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
- package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +6 -0
- package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -2
- package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
- package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
- package/dist/plugin/vite-plugin-config-loader.js +18 -9
- package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
- package/dist/plugin/vite-plugin-gql-tada.js +2 -2
- package/dist/plugin/vite-plugin-ui-config.js +3 -2
- package/package.json +8 -6
- package/src/app/app-providers.tsx +8 -8
- package/src/app/main.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
- package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
- package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
- package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -1
- package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
- package/src/app/routes/_authenticated/_products/products.tsx +1 -1
- package/src/app/routes/_authenticated.tsx +12 -1
- package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
- package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
- package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
- package/src/lib/components/data-table/data-table-types.ts +1 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +72 -23
- package/src/lib/components/data-table/data-table.tsx +23 -24
- package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
- package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
- package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
- package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
- package/src/lib/components/layout/nav-user.tsx +4 -4
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
- package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
- package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
- package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
- package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
- package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
- package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
- package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
- package/src/lib/components/shared/custom-fields-form.tsx +4 -3
- package/src/lib/components/shared/customer-selector.tsx +13 -14
- package/src/lib/components/shared/detail-page-button.tsx +2 -2
- package/src/lib/components/shared/entity-assets.tsx +3 -3
- package/src/lib/components/shared/navigation-confirmation.tsx +39 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +9 -1
- package/src/lib/components/shared/product-variant-selector.tsx +111 -0
- package/src/lib/components/shared/vendure-image.tsx +1 -1
- package/src/lib/components/ui/calendar.tsx +508 -63
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
- package/src/lib/framework/document-introspection/get-document-structure.ts +70 -11
- package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
- package/src/lib/framework/layout-engine/page-layout.tsx +4 -0
- package/src/lib/framework/page/list-page.tsx +23 -4
- package/src/lib/framework/page/use-detail-page.ts +1 -0
- package/src/lib/graphql/fragments.tsx +8 -0
- package/src/lib/index.ts +5 -5
- package/src/lib/providers/auth.tsx +12 -9
- package/src/lib/providers/channel-provider.tsx +1 -0
- package/src/lib/providers/server-config.tsx +7 -1
- package/src/lib/providers/user-settings.tsx +24 -0
- package/vite/utils/ast-utils.spec.ts +128 -0
- package/vite/utils/ast-utils.ts +119 -0
- package/vite/utils/config-loader.ts +410 -0
- package/vite/{schema-generator.ts → utils/schema-generator.ts} +7 -1
- package/vite/{ui-config.ts → utils/ui-config.ts} +2 -2
- package/vite/vite-plugin-admin-api-schema.ts +2 -2
- package/vite/vite-plugin-config-loader.ts +25 -13
- package/vite/vite-plugin-dashboard-metadata.ts +19 -15
- package/vite/vite-plugin-gql-tada.ts +2 -2
- package/vite/vite-plugin-ui-config.ts +3 -2
- package/dist/plugin/config-loader.js +0 -141
- package/src/lib/components/shared/asset-preview.tsx +0 -345
- package/vite/config-loader.ts +0 -181
- /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
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AppLayout } from '@/components/layout/app-layout.js';
|
|
2
|
-
import { createFileRoute, redirect } from '@tanstack/react-router';
|
|
2
|
+
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
|
|
3
3
|
import { AUTHENTICATED_ROUTE_PREFIX } from '@/constants.js';
|
|
4
4
|
import * as React from 'react';
|
|
5
|
+
import { useAuth } from '@/hooks/use-auth.js';
|
|
5
6
|
|
|
6
7
|
export const Route = createFileRoute(AUTHENTICATED_ROUTE_PREFIX)({
|
|
7
8
|
beforeLoad: ({ context, location }) => {
|
|
@@ -21,5 +22,15 @@ export const Route = createFileRoute(AUTHENTICATED_ROUTE_PREFIX)({
|
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
function AuthLayout() {
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
const { isAuthenticated } = useAuth();
|
|
27
|
+
|
|
28
|
+
if (!isAuthenticated) {
|
|
29
|
+
navigate({
|
|
30
|
+
to: '/login'
|
|
31
|
+
});
|
|
32
|
+
return <></>;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
return <AppLayout />;
|
|
25
36
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button.js';
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenu,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuItem,
|
|
6
|
+
DropdownMenuTrigger,
|
|
7
|
+
} from '@/components/ui/dropdown-menu.js';
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
DialogTrigger,
|
|
15
|
+
} from '@/components/ui/dialog.js';
|
|
16
|
+
import { DataTableFilterDialog } from '@/components/data-table/data-table-filter-dialog.js';
|
|
17
|
+
import { Column, ColumnDef } from '@tanstack/react-table';
|
|
18
|
+
import { PlusCircle } from 'lucide-react';
|
|
19
|
+
import { Trans } from '@/lib/trans.js';
|
|
20
|
+
import React, { useState } from 'react';
|
|
21
|
+
import { camelCaseToTitleCase } from '@/lib/utils.js';
|
|
22
|
+
|
|
23
|
+
export interface AddFilterMenuProps {
|
|
24
|
+
columns: Column<any, unknown>[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AddFilterMenu({ columns }: AddFilterMenuProps) {
|
|
28
|
+
const [selectedColumn, setSelectedColumn] = useState<ColumnDef<any> | null>(null);
|
|
29
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
30
|
+
|
|
31
|
+
const filterableColumns = columns.filter(column => column.getCanFilter());
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
35
|
+
<DropdownMenu>
|
|
36
|
+
<DropdownMenuTrigger asChild>
|
|
37
|
+
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
|
38
|
+
<PlusCircle className="mr-2 h-4 w-4" />
|
|
39
|
+
<Trans>Add filter</Trans>
|
|
40
|
+
</Button>
|
|
41
|
+
</DropdownMenuTrigger>
|
|
42
|
+
<DropdownMenuContent align="end" className="w-[200px]">
|
|
43
|
+
{filterableColumns.map(column => (
|
|
44
|
+
<DropdownMenuItem
|
|
45
|
+
key={column.id}
|
|
46
|
+
onSelect={() => {
|
|
47
|
+
setSelectedColumn(column);
|
|
48
|
+
setIsDialogOpen(true);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{camelCaseToTitleCase(column.id)}
|
|
52
|
+
</DropdownMenuItem>
|
|
53
|
+
))}
|
|
54
|
+
</DropdownMenuContent>
|
|
55
|
+
</DropdownMenu>
|
|
56
|
+
{selectedColumn && (
|
|
57
|
+
<DataTableFilterDialog column={selectedColumn as any} />
|
|
58
|
+
)}
|
|
59
|
+
</Dialog>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -29,7 +29,6 @@ export interface DataTableColumnHeaderProps {
|
|
|
29
29
|
export function DataTableColumnHeader({ headerContext, customConfig }: DataTableColumnHeaderProps) {
|
|
30
30
|
const { column } = headerContext;
|
|
31
31
|
const isSortable = column.getCanSort();
|
|
32
|
-
const isFilterable = column.getCanFilter();
|
|
33
32
|
|
|
34
33
|
const customHeader = customConfig.header;
|
|
35
34
|
let display = camelCaseToTitleCase(column.id);
|
|
@@ -40,7 +39,6 @@ export function DataTableColumnHeader({ headerContext, customConfig }: DataTable
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
const columSort = column.getIsSorted();
|
|
43
|
-
const columnFilter = column.getFilterValue();
|
|
44
42
|
const nextSort = columSort === 'asc' ? true : columSort === 'desc' ? undefined : false;
|
|
45
43
|
|
|
46
44
|
return (
|
|
@@ -57,17 +55,6 @@ export function DataTableColumnHeader({ headerContext, customConfig }: DataTable
|
|
|
57
55
|
</Button>
|
|
58
56
|
)}
|
|
59
57
|
<div>{display}</div>
|
|
60
|
-
|
|
61
|
-
{isFilterable && (
|
|
62
|
-
<Dialog>
|
|
63
|
-
<DialogTrigger asChild>
|
|
64
|
-
<Button size="icon-sm" variant="ghost">
|
|
65
|
-
<Filter className={columnFilter ? '' : 'opacity-50'} />
|
|
66
|
-
</Button>
|
|
67
|
-
</DialogTrigger>
|
|
68
|
-
<DataTableFilterDialog column={column} />
|
|
69
|
-
</Dialog>
|
|
70
|
-
)}
|
|
71
58
|
</div>
|
|
72
59
|
);
|
|
73
60
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Filter } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
import { CircleX } from 'lucide-react';
|
|
4
|
+
import { Badge } from '../ui/badge.js';
|
|
5
|
+
import { useLocalFormat } from '@/hooks/use-local-format.js';
|
|
6
|
+
import { ColumnDataType } from './data-table-types.js';
|
|
7
|
+
import { HumanReadableOperator } from './human-readable-operator.js';
|
|
8
|
+
|
|
9
|
+
export function DataTableFilterBadge({
|
|
10
|
+
filter,
|
|
11
|
+
onRemove,
|
|
12
|
+
dataType,
|
|
13
|
+
currencyCode,
|
|
14
|
+
}: {
|
|
15
|
+
filter: any;
|
|
16
|
+
onRemove: (filter: any) => void;
|
|
17
|
+
dataType: ColumnDataType;
|
|
18
|
+
currencyCode: string;
|
|
19
|
+
}) {
|
|
20
|
+
const [operator, value] = Object.entries(filter.value as Record<string, unknown>)[0];
|
|
21
|
+
return (
|
|
22
|
+
<Badge key={filter.id} className="flex gap-1 items-center" variant="secondary">
|
|
23
|
+
<Filter size="12" className="opacity-50" />
|
|
24
|
+
<div>{filter.id}</div>
|
|
25
|
+
<div className="text-muted-foreground"><HumanReadableOperator operator={operator} mode="short" /></div>
|
|
26
|
+
<FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
|
|
27
|
+
<button className="cursor-pointer" onClick={() => onRemove(filter)}>
|
|
28
|
+
<CircleX size="14" />
|
|
29
|
+
</button>
|
|
30
|
+
</Badge>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function FilterValue({ value, dataType, currencyCode }: { value: unknown, dataType: ColumnDataType, currencyCode: string }) {
|
|
35
|
+
const { formatDate, formatCurrency } = useLocalFormat();
|
|
36
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
37
|
+
return Object.entries(value as Record<string, unknown>).map(([key, value]) => (
|
|
38
|
+
<div key={key} className="flex gap-1 items-center">
|
|
39
|
+
<span className="text-muted-foreground">{key}: </span>
|
|
40
|
+
<FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
|
|
41
|
+
</div>
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex gap-1 items-center">
|
|
47
|
+
[
|
|
48
|
+
{value.map(v => (
|
|
49
|
+
<FilterValue value={v} dataType={dataType} currencyCode={currencyCode} key={v} />
|
|
50
|
+
))}
|
|
51
|
+
]
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'string' && isDateIsoString(value)) {
|
|
56
|
+
return <div>{formatDate(value, { dateStyle: 'short', timeStyle: 'short' })}</div>;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'boolean') {
|
|
59
|
+
return <div>{value ? 'true' : 'false'}</div>;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === 'number' && dataType === 'Money') {
|
|
62
|
+
return <div>{formatCurrency(value, currencyCode)}</div>;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'number') {
|
|
65
|
+
return <div>{value}</div>;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
return <div>{value}</div>;
|
|
69
|
+
}
|
|
70
|
+
return <div>{value as string}</div>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isDateIsoString(value: string) {
|
|
74
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value);
|
|
75
|
+
}
|